【问题标题】: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?与 auto_ptr 声明不同,unique_ptr 声明是否在其模板类型为不完整类型时是明确定义的?
【发布时间】:2012-09-27 17:07:43
【问题描述】:

我写了this article,上面写了一些让我很困惑的cmets。

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

struct T2;

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

如果我不继续在同一个 TU 中的某处定义 T2,这将调用 UB,因为 std::auto_ptr&lt;T2&gt; 在其内部 T2*calling delete on an pointer to an object of an incomplete type whose complete type has a non-trivial destructor is undefined 上调用 delete

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

我碰巧使用的 GCC 工具链 — v4.3.3 (Sourcery G++ Lite 2009q1-203) — 很友好地告诉我:

注意:析构函数和特定于类的操作符 delete 都不会被调用,即使它们是在定义类时声明的。

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

我的抱怨是,如果 deleteing 指向不完整类型实例的指针是格式错误而不是 UB,那么发现这样的错误会容易得多,但是对于实现来说似乎是一个难以解决的问题,所以我明白为什么它是 UB。

但后来有人告诉我,如果我改用std::unique_ptr&lt;T2&gt;,这将是安全且合规的。

n3035 据称在 20.9.10.2 说:

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

我只能在 C++11 中找到:

[C++11: 20.7.1.1.1]:

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

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

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

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


我想我的问题是这样的:

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

struct T2;

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

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

【问题讨论】:

  • 因为措辞是“程序格式错误”(没有明确的“并且不需要诊断”),而不是例如'这会导致未定义的行为',这确实意味着需要进行诊断(可以实现为static_assert( sizeof(T), "" ))。所以是的,std::unique_ptr&lt;T&gt;std::auto_ptr&lt;T&gt; 更安全。 (但请注意,这是由于使用了默认的 std::default_delete&lt;T&gt; 删除器。)
  • 我想明确指出,“代码格式正确且定义明确”/“存在 UB”并不是仅有的两种可能结果。 (这可能不是您想要传达的,但我宁愿明确。)

标签: c++ c++11 language-lawyer unique-ptr auto-ptr


【解决方案1】:

以下示例试图演示std::auto_ptr&lt;T&gt;std::unique_ptr&lt;T&gt; 之间的区别。首先考虑这个由 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 之间的区别在于,对于前者,直到运行时您才发现您需要进行一些调试(编译器可能给出的模可选警告)。使用后者,您可以保证在编译时找到。

【讨论】:

    【解决方案2】:

    根据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::unique_ptr&lt;T&gt; 有这个属性,而不是std::unique_ptr,我不会称之为问题。如果您愿意支付类型擦除的费用,您可以使用例如std::unique_ptr&lt;T, std::function&lt;void(T*)&gt;&gt; 并在构建时传递适当的删除器。
    • 另一种选择是 std::unique_ptr&lt;T, void(*)(T*)&gt;(与例如无捕获的 lambda 一起),其中成本是间接的。
    猜你喜欢
    • 1970-01-01
    • 2015-12-17
    • 2013-07-09
    • 2019-12-23
    • 2016-07-03
    • 2011-04-16
    • 1970-01-01
    • 2013-05-02
    • 1970-01-01
    相关资源
    最近更新 更多