与auto_ptr声明不同,当unique_ptr声明的模板类型不完整时,它是否是明确定义的?

Is it true that a unique_ptr declaration, unlike a auto_ptr declaration, is well-defined when its template type is of an incomplete type?

本文关键字:声明 ptr 是否是 定义 auto unique 类型      更新时间:2023-10-16

我写了这篇文章,得到了一些让我感到困惑的评论。

它基本上归结为我看到T2仅用作模板参数,并错误地得出结论,因此我可以借此机会进行前瞻性声明:

struct T2;
struct T1
{
std::auto_ptr<T2> obj;
};

如果我不继续在同一 TU 中的某处定义T2,这将调用 UB,因为std::auto_ptr<T2>在其内部T2*上调用delete,并且在指向不完整类型的对象的指针上调用delete,该对象的完整类型具有非平凡析构函数是未定义的:

[C++11: 5.3.5/5]:如果要删除的对象在删除时具有不完整的类类型,并且完整类具有非平凡析构函数或释放函数,则行为是未定义的。

我碰巧使用的 GCC 工具链 - v4.3.3(Sourcery G++ Lite 2009q1-203) - 很好心地让我知道:

注意:不会调用析构函数和特定于类的运算符 delete,即使在定义类时声明了它们也是如此。

尽管在其他 GCC 版本中似乎很难获得此诊断。

我的抱怨是,如果delete指向不完整类型实例的指针格式不正确而不是 UB,那么发现这样的错误会容易得多,但这似乎是实现解决的一个棘手问题,所以我理解为什么它是 UB。

但后来我被告知,如果我改用std::unique_ptr<T2>,这将是安全和合规的。

据称,N3035 在 20.9.10.2 中说:

模板参数Tunique_ptr可能是不完整的类型。

我在 C++11 中所能找到的只是:

[C++11: 20.7.1.1.1]:

/1类模板default_delete用作类模板unique_ptr的默认删除程序(销毁策略)。

/2模板参数Tdefault_delete可能是不完整的类型。

但是,default_deleteoperator()确实需要一个完整的类型:

[C++11: 20.7.1.1.2/4]:如果T是不完整的类型,则程序格式不正确。


我想我的问题是这样的:

我文章的评论者说仅由以下代码组成的翻译单元格式良好且定义良好是否正确?还是他们错了?

struct T2;
struct T1
{
std::unique_ptr<T2> obj;
};

如果它们是正确的,那么编译器应该如何实现这一点,因为至少有充分的理由让它成为 UB,至少在使用std::auto_ptr时是这样?

根据GOTW #100 中的 Herb Sutter 的说法,unique_ptr在不完整类型方面遇到了与auto_ptr相同的问题。

。尽管unique_ptr和shared_ptr都可以使用 不完整类型,unique_ptr 的析构函数需要完整类型 以调用删除...

他的建议是声明包含类的析构函数(即T1),然后将其定义放在翻译单元中,其中T2是完整类型。

// T1.h
struct T2;
struct T1
{
~T1();
std::unique_ptr< T2 >;
};
// T1.cpp
#include "T2.h"
T1::~T1()
{
}

下面的示例尝试演示std::auto_ptr<T>std::unique_ptr<T>之间的区别。 首先考虑这个由 2 个源文件和 1 个标头组成的程序:

标题:

// test.h
#ifndef TEST_H
#define TEST_H
#include <memory>
template <class T>
using smart_ptr = std::auto_ptr<T>;
struct T2;
struct T1
{
smart_ptr<T2> obj;
T1(T2* p);
};
T2*
source();
#endif  // TEST_H

第一个来源:

// test.cpp
#include "test.h"
int main()
{
T1 t1(source());
}

第二个来源:

// test2.cpp
#include "test.h"
#include <iostream>

struct T2
{
~T2() {std::cout << "~T2()n";}
};
T1::T1(T2* p)
: obj(p)
{
}
T2*
source()
{
return new T2;
}

这个程序应该编译(它可能编译时带有警告,但它应该编译)。 但在运行时,它展示了未定义的行为。 它可能不会输出:

~T2()

这表示T2的析构函数尚未运行。 至少它不在我的系统上。

如果我将 test.h 更改为:

template <class T>
using smart_ptr = std::unique_ptr<T>;

然后,编译器需要输出诊断(错误)。

也就是说,当您在使用auto_ptr犯此错误时,会出现运行时错误。 当您使用unique_ptr犯此错误时,会出现编译时错误。这就是auto_ptrunique_ptr之间的区别。

若要修复编译时错误,必须在T2完成后概述~T1()。 在 test2 中.cpp在T2后添加:

T1::~T1() = default;

现在它应该编译并输出:

~T2()

您可能还需要声明和分级显示移动成员:

T1::T1(T1&&) = default;
T1& T1::operator=(T1&&) = default;

您可以使用auto_ptr进行相同的修复,并且再次是正确的。 但同样,auto_ptrunique_ptr之间的区别在于,对于前者,您直到运行时才会发现您有一些调试要做(编译器可能会给出的模可选警告)。 使用后者,您可以保证在编译时找到答案。