gcc 为什么shared_ptr ref count的impl持有一个指向实际指向类型的指针?

rbl8hiat  于 2022-11-13  发布在  其他
关注(0)|答案(3)|浏览(109)

这是由一个采访问题激发的:

shared_ptr<void> p(new Foo());

一旦p超出作用域,Foo的析构函数是否会被调用?
事实证明是这样的,我不得不查看GCC 1中shared_ptr的实现,并发现显然控制块保存了一个指向实际类型(Foo)的指针和一个指向析构函数的指针,当ref计数达到0时,析构函数被调用。
1:对不起,我在我的手机上,我无法复制到impl的链接。
但我还是纳闷:为什么?2为什么需要它?3我在标准中遗漏了什么吗?
另一方面,上面的代码行不能用unique_ptr编译,因为在这种情况下显然没有引用计数。

1tuwyuhd

1tuwyuhd1#

.get()被调用时,std::shared_ptr<T>示例本身必须跟踪要返回的指针。它的类型总是T*,除非T是数组,在这种情况下,它的类型是std::remove_extent_t<T>*(例如,std::shared_ptr<int[]>::get()返回int*)。
同样,当std::shared_ptr<T>被销毁时,它必须检查它是否是引用它的控制块的最后一个std::shared_ptr示例。如果是,它必须执行删除器。为了使它工作,控制块必须跟踪传递给删除器的指针。它 * 不一定 * 是T*std::remove_extent_t<T>*类型。
它们不相同的原因是,例如,类似下面的代码应该可以工作:

struct S {
    int member;
    int other_member;
    ~S();
};
void foo(std::shared_ptr<int>);
int main() {
    std::shared_ptr<S> sp = std::make_shared<S>();
    std::shared_ptr<int> ip(sp, &sp->member);
    foo(std::move(ip));
}

这里,sp拥有一个S类型的对象,并且也指向同一个对象。函数foo使用std::shared_ptr<int>,因为它是某个API的一部分,该API需要一个int对象,只要API没有使用它,该对象就将保持活动状态(但是如果调用者愿意的话,他们也可以让它保持更长的活动时间)。fooAPI不关心您给予它的int是否是某个更大对象的一部分;它只关心在保存int时它不会被破坏。因此,我们可以创建一个名为ipstd::shared_ptr<int>,它指向sp->member,并将其传递给foo。现在,这个int对象只有在包含的S对象是活动的时候才能存在。因此,只要ip是活动的,它就必须使 * 整个 * S对象保持活动状态。我们现在可以调用sp.reset(),但是S对象必须保持活动状态,因为仍然有一个shared_ptr引用它。最后,当ip被销毁时,它必须销毁 * 整个 * S对象,而不仅仅是它本身所指向的int。因此,对于std::shared_ptr<int>示例ip来说,存储int*(当调用.get()时将返回)是不够的;它所指向的控制块也必须存储X1 M37 N1 X以传递到删除器。
出于同样的原因,您的代码将调用Foo析构函数,即使它是一个执行析构的std::shared_ptr<void>
你问:“我在标准中遗漏了什么吗?”我认为你是在问标准是否需要这种行为,如果需要,在标准中是在哪里规定的?答案是肯定的。标准规定std::shared_ptr<T> * 存储 * 指针,也可以 * 拥有 * 指针;这两个指针不需要相同。特别地,[util.smartptr.shared.const]/14描述了“[构造]一个shared_ptr示例,该示例 * 存储 * p并 * 与 * r的初始值 * 共享所有权”的构造函数。这样创建的shared_ptr示例可能拥有一个与它存储的指针不同的指针。但是,当它被销毁时,[util.smartptr.shared.dest]/1适用:如果这是最后一个示例,则删除 owned 指针(而不是 stored 指针)。

t40tm48m

t40tm48m2#

我假设对于这段代码来说,答案是微不足道的:

shared_ptr<Foo> p(new Foo());

每个对new的调用都必须由对delete的调用来平衡。每个构造的对象也必须被析构。因此,如果

shared_ptr<void> p(new Foo());

我不会调用~Foo(),这会令人相当惊讶,并会导致资源泄漏、悬空指针或任何数量的UB,因为析构函数没有被调用。
对我来说,更大的问题是:shared_ptr有错误的类型,所以它不应该调用正确的析构函数,这样就不应该编译(就像unique_ptr失败一样)。
原因是我相信这一点天才:

template< class Y > shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
template< class Y > shared_ptr( shared_ptr<Y>&& r, element_type* ptr ) noexcept;

您可以建立指向较大对象之成员的共用指标,只要指向成员的指标存在,就会让较大对象保持作用中。
为了使这个特性起作用,shared_ptrshared_ptr的控制块都有一个指针,它们可以有不同的类型。控制块总是指向对象,而shared_ptr指向成员。当你正常地创建一个shared_ptr时,它们碰巧是相同的类型,指向相同的地址。但显然情况并不总是如此。
这也允许用指向Foo的控制块生成一个shared_ptr<void>。这里两者指向相同的地址,但具有不同的类型。控制块知道原始对象的类型以及最后要调用的析构函数。
shared_ptr和控制块可以有不同类型的指针,这就允许使用以下复制构造函数:

template< class Y > shared_ptr( const shared_ptr<Y>& r ) noexcept;
template< class Y > shared_ptr( shared_ptr<Y>&& r ) noexcept;

只要Y*可以转换/兼容T*,你就可以在复制构造过程中改变shared_ptr的类型。

shared_ptr<void> p(shared_ptr<Foo>(new Foo()));

它创建临时shared_ptr<Foo>,其中控制块具有Foo*,然后p重用相同的控制块。

osh3o9ms

osh3o9ms3#

std::shared_ptr::shared_ptr - cppreference.com
| | |
| - -|- -|
| constexpr shared_ptr() noexcept;|(上)|
| constexpr shared_ptr( std::nullptr_t ) noexcept;个|(下)|
| template< class Y > explicit shared_ptr( Y* ptr );|(三)|
| ......|一个人。|
....
3-7)建构shared_ptr,其中ptr做为Managed对象的指标。
| | |
| - -|- -|
| 对于 (3- 4,6)Y*必须可转换为T*。|(C17之前)|
| 如果TU[N]类型的数组,则 (3- 4,6) 如果Y(*)[N]无法转换为T*,则不参与重载解析。如果T是数组类型U[](3- 4,6) 如果Y(*)[]不能转换为T*,则不参与重载解析。否则,(3- 4,6) 如果Y*不能转换为T*,则不参与重载解析。|(自C
17起)|
此外:
1.如果T不是数组类型,则使用**delete-expression**delete指针; delete[] ptr,如果T是数组类型 (C++17起) 作为删除器。Y必须是完整类型。删除表达式必须格式正确,具有定义良好的行为,并且不引发任何异常。此外,如果删除表达式格式不正确,此构造函数不参与重载解析。(C++17起)
所以基本上使用了第三种形式。
同样,保存引用计数器(强和弱)的数据也保存了对象的析构函数的信息。构造函数的(3)形式获取这些信息。
请注意,std::unique_ptr在默认情况下不保存此类信息,因此在此情况下将失败(无法编译)。

相关问题