unique_ptr在构造函数中为什么需要完整的类型?

huangapple go评论63阅读模式
英文:

Why unique_ptr requires complete type in constructor?

问题

Is std::unique_ptr<T> required to know the full definition of T?这个问题中,我知道如果一个类A有一个成员unique_ptr&lt;T&gt;,那么在析构函数~A()中,T必须是一个完整的类型。然而,我遇到了这样一种情况,构造函数A()也要求T的完整类型,如下面的代码所示:

// a.h -------------------------------
#pragma once
#include &lt;memory&gt;
struct B;
struct A {
  A(); // &lt;---
  ~A();
  std::unique_ptr&lt;B&gt; ptr;
};

// a.cpp -------------------------------
#include &quot;a.h&quot;
struct B {};
A::A() = default; // &lt;---
A::~A() = default;

// main.cpp -------------------------------
#include &quot;a.h&quot;
int main() {A a;}

如果将构造函数A::A()的定义移到头文件a.h中,编译器将报错error: invalid application of ‘sizeof’ to incomplete type ‘B’。为什么会发生这种情况?是否有相关的参考资料?

顺便说一句,我正在使用Ubuntu 18.04上的gcc-7.5.0,并启用了c++17。

英文:

From Is std::unique_ptr<T> required to know the full definition of T?, I know that if a class A has a member unique_ptr&lt;T&gt;, then T shall be a complete type in destructor ~A(). However, I came across a situation that the constructor A() also requires complete type of T, see code below:

// a.h -------------------------------
#pragma once
#include &lt;memory&gt;
struct B;
struct A {
  A(); // &lt;---
  ~A();
  std::unique_ptr&lt;B&gt; ptr;
};

// a.cpp -------------------------------
#include &quot;a.h&quot;
struct B {};
A::A() = default; // &lt;---
A::~A() = default;

// main.cpp -------------------------------
#include &quot;a.h&quot;
int main() {A a;}

If the definition of constructor A::A() is moved to the header a.h, the compiler will complain error: invalid application of ‘sizeof’ to incomplete type ‘B’. Why is this happening? Is there any reference material about this?

BTW, I'm using gcc-7.5.0 on Ubuntu 18.04, with c++17 enabled.


Edit for @463035818_is_not_a_number in the comments. The complete error message is:

[1/2] Building CXX object CMakeFiles/t.dir/main.cpp.o
FAILED: CMakeFiles/t.dir/main.cpp.o 
/usr/bin/c++   -g -fdiagnostics-color=always -std=gnu++1z -MD -MT CMakeFiles/t.dir/main.cpp.o -MF CMakeFiles/t.dir/main.cpp.o.d -o CMakeFiles/t.dir/main.cpp.o -c /home/user/Tests/UniquePtrTest/main.cpp
In file included from /usr/include/c++/7/memory:80:0,
                 from /home/user/Tests/UniquePtrTest/a.h:2,
                 from /home/user/Tests/UniquePtrTest/main.cpp:1:
/usr/include/c++/7/bits/unique_ptr.h: In instantiation of ‘void std::default_delete&lt;_Tp&gt;::operator()(_Tp*) const [with _Tp = B]’:
/usr/include/c++/7/bits/unique_ptr.h:263:17:   required from ‘std::unique_ptr&lt;_Tp, _Dp&gt;::~unique_ptr() [with _Tp = B; _Dp = std::default_delete&lt;B&gt;]/home/user/Tests/UniquePtrTest/a.h:5:3:   required from here
/usr/include/c++/7/bits/unique_ptr.h:76:22: error: invalid application of ‘sizeof’ to incomplete type ‘B’
  static_assert(sizeof(_Tp)&gt;0,
                      ^
ninja: build stopped: subcommand failed.

答案1

得分: 10

代码中提到的问题是A::A()需要知道如何销毁ptr,以防构造函数抛出异常。

一个示例代码如下:

#include <memory>
struct B {};

struct X{
    X(){throw 42;}
};

struct A {
  A() {}
  ~A() {};
  std::unique_ptr<B> ptr;
  X x;
};


int main() {
    A a;
}

生成的汇编代码如下:

A::A() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     QWORD PTR [rbp-24], rdi
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    std::unique_ptr<B, std::default_delete<B>>::unique_ptr<std::default_delete<B>, void>()
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 8
        mov     rdi, rax
        call    X::X() [complete object constructor]
        jmp     .L6
        mov     rbx, rax
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    std::unique_ptr<B, std::default_delete<B>>::~unique_ptr() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume

可以看到其中调用了std::unique_ptr<B, std::default_delete<B>>::~unique_ptr()

关于这个问题,你可以查阅C++标准文档,它定义了哪些函数/表达式需要完整类型。

实际上,没有太多关于这个问题的参考材料,因为你需要阅读C++标准文档来了解哪些函数/表达式需要完整类型。

当然,cppreference是一个质量较高且易读的参考资料,尽管我没有在那里找到这个特定用例。

特别是,这个问题在一个注释中提到了:
20.11.1.3.3 Destructor [unique.ptr.single.dtor]

[注 1:使用default_delete要求T是一个完整类型。 — 结束注]

英文:

The issue is that A::A() needs to know how to destroy ptr in case the constructor throws.

An example:

#include &lt;memory&gt;
struct B {};

struct X{
    X(){throw 42;}
};

struct A {
  A() {}
  ~A() {};
  std::unique_ptr&lt;B&gt; ptr;
  X x;
};


int main() {
    A a;
}

generates:

A::A() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     QWORD PTR [rbp-24], rdi
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    std::unique_ptr&lt;B, std::default_delete&lt;B&gt; &gt;::unique_ptr&lt;std::default_delete&lt;B&gt;, void&gt;()
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 8
        mov     rdi, rax
        call    X::X() [complete object constructor]
        jmp     .L6
        mov     rbx, rax
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    std::unique_ptr&lt;B, std::default_delete&lt;B&gt; &gt;::~unique_ptr() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume

showing the call to std::unique_ptr&lt;B, std::default_delete&lt;B&gt; &gt;::~unique_ptr().

> Is there any reference material about this?

Technically, yes, you can read the Standard which defines which functions/expressions require a complete type.

Practically, not so much, since you have to read the Standard which defines which functions/expressions require a complete type.

Of course cppreference is of high quality and actually readable, although I did not find this use case there.

In particular, this issue is mentioned in a note
20.11.1.3.3 Destructor [unique.ptr.single.dtor]
> [Note 1 : The use of default_delete requires T to be a complete type. — end note]

答案2

得分: 3

你看到的错误是std::default_deleter针对未定义行为的保护。

当你实例化构造函数std::unique_ptr&lt;B&gt;::unique_ptr的定义时,也会实例化std::default_delete&lt;B&gt;::operator()的定义。在其中有一个断言

static_assert(sizeof(B) &gt; 0);

它检查不完整类型。这可以防止删除不完整类型,这是未定义的行为。参见https://stackoverflow.com/questions/71821115/incomplete-types-with-shared-ptr-and-unique-ptr。

但是,为什么将A::A()的定义移到头文件中会导致错误,而将其放在实现文件中却不会呢?

事实证明,只声明成员std::unique_ptr&lt;B&gt;只会实例化其构造函数的_声明_,而不会实例化其定义。因此,如果A::A()在实现文件中定义,那么std::default_delete&lt;B&gt;::operator()的定义也只有在那时才会被实例化,此时B是一个完整的类型。

英文:

The error you see is std::default_deleter guarding against undefined behaviour for you.

When you instantiate the definition of the constructor std::unique_ptr&lt;B&gt;::unique_ptr, the definition of std::default_delete&lt;B&gt;::operator() is also instantiated. Within which is an assertion

static_assert(sizeof(B) &gt; 0);

which checks for incomplete types. This prevents any possible deletion of an incomplete type, which is undefined behaviour. See also https://stackoverflow.com/questions/71821115/incomplete-types-with-shared-ptr-and-unique-ptr.

But why does moving the definition of A::A() to the header cause an error but not if it's in the implementation file?

As it turns out, simply declaring the member std::unique_ptr&lt;B&gt; only instantiates the declaration of its constructor but not the definition. Therefore if A::A() is defined in the implementation file, the definition of std::default_delete&lt;B&gt;::operator() is also only instantiated then, by which B is a complete type.

huangapple
  • 本文由 发表于 2023年2月10日 16:36:01
  • 转载请务必保留本文链接:https://go.coder-hub.com/75408648.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定