如果 std::shared_ptr 是从非 null 构造的,为什么它不需要知道完整的类型?

Why doesn't std::shared_ptr need to know complete type if it's constructed from non-null?

本文关键字:不需要 为什么 类型 ptr shared std null 如果      更新时间:2023-10-16

我在factory.h中有一个工厂函数,它将std::shared_ptr返回到foo.h中的基类。factory.h使用基类的前向声明,而不是包含foo.h。作为以下代码:

工厂.h

#include <memory>
// forward declaration
class foo;
std::shared_ptr<foo> create_foo_A(int A);
std::shared_ptr<foo> create_foo_B(int A, int B);
void work_with_foo(std::shared_ptr<foo> ptr);

在客户端代码中,如果使用nullptr初始化std::shared_ptr<foo>,编译器将发出警告。

主.cpp

#include "factory.h"
int main()
{
int type = 1;

std::shared_ptr<foo> ptr(nullptr);    // <--- compile warning
if (type == 1)
ptr = create_foo_A(5566);
else
ptr = create_foo_B(5566, 7788);
work_with_foo(ptr);

return 0;
}

警告消息为:

warning C4150 : deletion of pointer to incomplete type 'foo'; no destructor called

这是合理的,因为std::shared_ptr不知道完整的foo类型。如果main.cpp包含foo.h,则可以删除此警告。

但是如果std::shared_ptr初始化为非nullptr,编译器不会发出警告。

主.cpp

#include "factory.h"
int main()
{
int type = 1;

if (type == 1)
{
std::shared_ptr<foo> ptr = create_foo_A(5566);    // <--- OK
work_with_foo(ptr);
}
else
{
std::shared_ptr<foo> ptr = create_foo_B(5566, 7788);    // <--- OK
work_with_foo(ptr);
}

return 0;
}

在这个场景中,为什么std::shared_ptr不需要知道完整的类foo类型?当std::shared_ptrnullptr和非nullptr构造时,它有很大的不同吗?

这个:

std::shared_ptr<foo> ptr(nullptr);

不应该警告。 这是一个完全有效的陈述,不需要知道foo的完整类型。 它很可能是在警告您,因为正在模拟nullptr。 这也不应该警告:

std::shared_ptr<foo> ptr;

并且两者被指定为等效:

constexpr shared_ptr(nullptr_t) : shared_ptr() { }

在这个场景中,为什么 std::shared_ptr 不需要知道完整的类 foo 类型?

create_foo_Acreate_foo_B的定义需要知道foo的完整类型。 但宣言没有。

简而言之,shared_ptr<T>::shared_ptr(U*)构造函数需要知道U的完整定义。 但除此之外,别无他法。 这个答案中有一个更完整的调查:

https://stackoverflow.com/a/6089065/576911

所以如果你说:

std::shared_ptr<foo> ptr((foo*)0);

然后你有未定义的行为。 显然VC++会发出警告。 libc++给出了一个硬错误。 而使用nullptr是完全可以的,至少根据 C++11。

总之,您的所有示例都不需要foo的完整定义。


[util.smartptr.shared.dest]

~shared_ptr();

影响:

  • 如果*this为空或与另一个shared_ptr实例(use_count() > 1)共享所有权,则没有副作用。

[util.smartptr.shared.const]

constexpr shared_ptr() noexcept;

效果:构造一个空shared_ptr对象。


[util.smartptr.shared]

在剧情简介中:

constexpr shared_ptr(nullptr_t) : shared_ptr() { }

所以std::shared_ptr是三件事包装在一个整洁的小包装中。

首先,它是指向某些数据的指针,具有覆盖以允许您访问它,这使得使用它与使用原始 C 指针非常相似。

第二部分是参考计数器。 它跟踪相当于一个std::atomic<unsigned>,当它们进入/超出范围时,所有std::shared_ptr都会从第一个递增/递减中复制出来。

第三部分是破坏函数 - 或者更准确地说是指向所述破坏函数的指针。

当您从另一个std::shared_ptr复制一个时,它使用引用计数器/销毁函数/指向源std::shared_ptr数据的指针。 所以它不需要编写销毁函数,也不需要知道销毁函数是做什么的!

创建std::shared_ptr<Foo>而不复制它时,它必须创建销毁函数。 默认的销毁函数是调用Foo的析构函数:这需要完整声明Foo

即使指针nullptrstd::shared_ptr也会调用销毁函数,因此即使在的情况下,它也需要编写该销毁函数。

所以简短的故事是:无论你在任何地方创建一个std::shared_ptr<Foo>,而不是通过复制或从另一个std::shared_ptr移动,你必须要么提供一个手动销毁功能,要么你必须有Foo::~Foo可见的(即,Foo的完整声明)。

调用返回std::shared_ptr的函数的情况是从函数主体内部复制的:在该主体中,无论这些std::shared_ptr最终来自何处,都满足上述两个要求之一。

您只需在nullptr上创建它的情况,它必须编写并存储销毁函数,这需要对该类的完全访问权限。

如果您确实需要在无法访问foo的情况下std::shared_ptr<foo> ptr(nullptr);,请尝试std::shared_ptr<foo> ptr(nullptr, [](foo*){});,它应该传递一个无所事事的破坏函数。

恐怕你的问题完全是倒退的。 :-)

在第二个示例中,您调用 create_foo_A() 和 create_foo_B(),您实际上是在向编译器承诺,这些函数在链接到程序中时,将返回指向派生自 foo 的类实例的指针。 编译器不需要知道或关心 foo 的组成; 在这一点上,由这些函数来决定 foo 到底是什么。

但是在您的第一个示例中,该行:

std::shared_ptr<foo> ptr(nullptr);

构造指向 foo 的共享指针,程序期望为要调用的共享指针找到析构函数。 由于您没有定义 foo,只声明它是一个类,编译器不知道该类是什么样子的(它是一个不完整的类型),并抱怨没有该析构函数。