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

4

通過 是否需要知道T的完整定義來使用std::unique_ptr?,我知道如果一個類A有一個成員unique_ptr<T>,那麼在析構函數~A()中,T必須是一種完整的類型。然而,我遇到了一種情況,即構造函數A()還要求T的完整類型,請參見下面的代碼:

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

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

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

如果将构造函数 A::A() 的定义移动到头文件 a.h 中,编译器会报错 error: invalid application of ‘sizeof’ to incomplete type ‘B’。为什么会发生这种情况?是否有相关的参考资料?
顺便说一下,我正在使用启用了 c++17 的 Ubuntu 18.04 上的 gcc-7.5.0。

针对评论中的 @463035818_is_not_a_number 进行编辑。完整的错误信息如下:

[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<_Tp>::operator()(_Tp*) const [with _Tp = B]’:
/usr/include/c++/7/bits/unique_ptr.h:263:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = B; _Dp = std::default_delete<B>]’
/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)>0,
                      ^
ninja: build stopped: subcommand failed.

1
构造函数需要知道如何销毁所有字段,在构造过程中如果抛出异常的话。 - Matteo Italia
@MatteoItalia 看起来你的解释应该适用于在类声明中(在h文件中)使用相同的A() = default。但是,如果你这样做,编译器不会报错。 - wohlstad
1
@wohlstad - 编译为C++20,这样你就不必处理愚蠢的聚合了。 - StoryTeller - Unslander Monica
2
顺便说一下,#include 基本上只是替换文本,因此应该可以在单个文件中使用代码重现它。这样做可以使其他人更容易地复制。 - 463035818_is_not_a_number
1
请参见 https://dev59.com/wcTra4cB1Zd3GeqPySOn。 - Passer By
显示剩余10条评论
2个回答

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()

有关此事的参考资料吗?

从技术上讲,可以阅读标准,标准定义了哪些函数/表达式需要完整类型。

实际上,这并不多见,因为您必须阅读标准,才能了解哪些函数/表达式需要完整类型。

当然 cppreference 质量很高,而且易于阅读,尽管我没有在那里找到此用例。

特别地,该问题在注释中提到 20.11.1.3.3 析构函数 [unique.ptr.single.dtor]

[注1:使用 default_delete 需要 T 是完整类型。——编者注]


1
谢谢。看起来我的问题是:如果在a.h中定义了A::A(),则构造函数不会在a.o中编译。然后,main.o从头文件编译A::A(),而此时B是不完整的类型。 - VictorBian
@VictorBian 是的,正是因为这样,定义才在带有不完整 B 的头文件内联。抱歉,应该明确说明一下。如果你将 main 放在 a.cpp 中,我认为它会起作用。 - Quimby

3
你看到的错误是 std::default_deleter 为你防范未定义行为。
当你实例化构造函数 std::unique_ptr<B>::unique_ptr 的定义时,也会实例化 std::default_delete<B>::operator() 的定义。其中包含一个断言。
static_assert(sizeof(B) > 0);

该函数检查不完整类型。这可以防止删除未定义行为的不完整类型。另请参见使用shared_ptr和unique_ptr的不完整类型

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

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


谢谢你的回答。我认为你和@Quimby一起回答了我的问题。 - VictorBian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接