PIMPL惯用法在使用std::unique_ptr时是否有效?

5

我一直在尝试使用unique_ptr实现PIMPL惯用语法。我受到了几篇文章的启发,这些文章总是强调同一个重要点:只在实现PIMPL的类的头文件中声明析构函数,然后在.cpp文件中定义它。否则,你会得到编译错误,例如“不完整的类型bla bla”。

好吧,我按照这个规则在一个小测试中实现了它,但我仍然遇到了“不完整的类型”错误。下面是代码,非常简短。

A.hpp:

#pragma once
#include <memory>

class A
{
public:
  A();
  ~A();
private:
  class B;
  std::unique_ptr<B> m_b = nullptr;
};

A.cpp:

#include "A.hpp"

class A::B
{

};

A::A()
{

}

A::~A() // could be also '= default'
{

}

main.cpp:

#include "A.hpp"

int main()
{
  A a1;

  return 0;
}

我采用了两种(快速而简略的)方式进行构建,结果从我的角度来看非常惊人。

首先,我在没有链接A.cpp的情况下进行了构建。

g++ -c A.cpp

到目前为止没有任何错误。

然后,我将A.cpp和main.cpp编译成可执行文件。

g++ A.cpp main.cpp -o test

这就是我遇到麻烦的地方。在这里,我遇到了著名的“不完整类型”错误:
In file included from /usr/include/c++/9/memory:80,
                 from A.hpp:2,
                 from test.cpp:2:
/usr/include/c++/9/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = A::B]’:
/usr/include/c++/9/bits/unique_ptr.h:292:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = A::B; _Dp = std::default_delete<A::B>]’
A.hpp:11:28:   required from here
/usr/include/c++/9/bits/unique_ptr.h:79:16: error: invalid application of ‘sizeof’ to incomplete type ‘A::B’
   79 |  static_assert(sizeof(_Tp)>0,
      |                ^~~~~~~~~~~

我知道当你想要将unique_ptr用作PIMPL习惯用法的一部分时存在的约束条件,并且我尝试过关注它们。然而,在这种情况下,我不得不承认我没有主意了(这让我感到非常烦躁)。

我是做错了什么,还是我们不能在这种情况下使用unique_ptr?

在线演示


删除复制构造函数,声明/删除移动构造函数,同样适用于赋值运算符。 - Marek R
也许是重复的:https://dev59.com/KWox5IYBdhLWcg3wtmg1。其中未被接受的答案更加完整。 - 463035818_is_not_a_number
@MarekR 这是一个好主意,但我认为在这种情况下不应该改变任何东西。由于 unique_ptr,复制操作不存在。由于自定义析构函数,移动操作也不存在。 - HolyBlackCat
这与类体中的 = nullptr 有关。如果将其删除,它就可以正常工作。 - HolyBlackCat
1
进一步阅读:https://herbsutter.com/gotw/_100/ - ildjarn
显示剩余2条评论
1个回答

6
我还没有完全理解这个问题,但是原因是m_ptr成员的默认成员初始化器。如果你使用成员初始化列表,它将会编译通过,没有错误:
// A.hpp:
class A
{
public:
  A();
  ~A();
private:
  class B;
  std::unique_ptr<B> m_b; // no initializer here
};

// A.cpp:
A::A() : m_b(nullptr)     // initializer here
{

}

https://wandbox.org/permlink/R6SXqov0nl7okAW0

注意,clang的错误信息更好地指出了导致错误的行:
In file included from prog.cc:1:
In file included from ./A.hpp:3:
In file included from /opt/wandbox/clang-13.0.0/include/c++/v1/memory:682:
In file included from /opt/wandbox/clang-13.0.0/include/c++/v1/__memory/shared_ptr.h:25:
/opt/wandbox/clang-13.0.0/include/c++/v1/__memory/unique_ptr.h:53:19: error: invalid application of 'sizeof' to an incomplete type 'A::B'
    static_assert(sizeof(_Tp) > 0,
                  ^~~~~~~~~~~
/opt/wandbox/clang-13.0.0/include/c++/v1/__memory/unique_ptr.h:318:7: note: in instantiation of member function 'std::default_delete<A::B>::operator()' requested here
      __ptr_.second()(__tmp);
      ^
/opt/wandbox/clang-13.0.0/include/c++/v1/__memory/unique_ptr.h:272:19: note: in instantiation of member function 'std::unique_ptr<A::B>::reset' requested here
  ~unique_ptr() { reset(); }
                  ^
./A.hpp:12:28: note: in instantiation of member function 'std::unique_ptr<A::B>::~unique_ptr' requested here
  std::unique_ptr<B> m_b = nullptr;
                           ^
./A.hpp:11:9: note: forward declaration of 'A::B'
  class B;
        ^
1 error generated.

是的,它确实解决了问题,但我也不明白为什么...... 我可能会将其标记为已接受的答案。但是,如果您找到了答案,我会很高兴您更新一下 :) - PlayerK
2
@PlayerK 我仍在进行实验,尽管 std::unique_ptr<B> m_b{nullptr}; 也可以工作。似乎 std::unique_ptr<B> m_b = nullptr; 由于某种原因需要在 A.hpp 中已经有析构函数。实际上,我希望这是编译器的错误,因为在初始化期间不应该需要析构函数。 - 463035818_is_not_a_number
这是因为构造发生的时间。如果初始化程序在头文件中,则成员在调用构造函数体之前被构造。因此,如果有异常,它将被销毁。对象必须在销毁位置处“完整”。这意味着不能出现部分销毁的情况。 - Mgetz
@PlayerK 我写了一个后续问题,希望有人能解释 https://dev59.com/DlEG5IYBdhLWcg3wLFwk - 463035818_is_not_a_number
3
没有必要显式地将unique_ptr初始化为nullptr。只需使用unique_ptr的默认构造函数,让它在内部自行初始化为nullptr即可。 - Remy Lebeau
显示剩余3条评论

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