使用 pimpl 成语移动操作

时间:2021-08-01 22:07:13

标签: c++ unique-ptr move-semantics pimpl-idiom

在下面的代码中,我试图在 PIMPL 惯用法中使用移动赋值,但代码无法编译。

struct.hpp:

#pragma once

#include <memory>

struct A {
  std::unique_ptr<struct B> m_x;
  A(int x);
  ~A();
};

struct.cpp:

#include "struct.hpp"

struct B {
  int x;
};

A::A(int x) : m_x{new B} { m_x->x = x; }
A::~A() = default;

main.cpp:

#include <utility>
#include "struct.hpp"

int main()
{
  A a(2);
  A b(3);
  a = std::move(b);
  return 0;
}

虽然 struct.cpp 编译时没有警告,但 ```main.cpp`` 没有,给出错误:

$ g++ -c -std=c++17 -o main.o main.cpp
main.cpp: In function ‘int main()’:
main.cpp:8:18: error: use of deleted function ‘A& A::operator=(const A&)’
    8 |   a = std::move(b);
... (etc) ...

很明显,复制分配 A::operator=(const A&) 被删除,因为它是为 std::unique_ptr 删除的。 但是为什么编译器首先尝试使用它? std::move 不应该强制使用对 std::unique_ptr 有效且定义的移动赋值吗?

1 个答案:

答案 0 :(得分:3)

虽然 std::unique_ptr 确实有一个移动赋值运算符,而且想要利用这一事实使 A 可移动赋值似乎很自然,但用户声明的构造函数遇到了问题。

move assignment operator 上的 cppreference:

<块引用>

隐式声明的移动赋值运算符

如果没有为类类型(structclassunion)提供用户定义的移动赋值运算符,并且以下所有条件都为真:

  • 没有用户声明的复制构造函数;
  • 没有用户声明的移动构造函数;
  • 没有用户声明的复制赋值运算符;
  • 没有用户声明的析构函数,

然后编译器会将移动赋值运算符声明为其类的 inline 公共成员,其签名为 T& T::operator=(T&&)

注意最后一点:A 有一个用户声明的析构函数,所以你不会得到隐式声明的移动赋值运算符。

如果我们想以最少的努力使 A 可移动赋值,我们可以显式声明移动赋值运算符并请求默认实现如下:

struct.hpp:

#include <memory>

struct A {
  std::unique_ptr<struct B> m_x;
  A(int x);
  A& operator=(A&&) noexcept;
  ~A();
};

struct.cpp:

#include "struct.hpp"

struct B {
    int x;
};

A::A(int x) : m_x{ new B } { m_x->x = x; }
A::~A() = default;
A& A::operator=(A&&) noexcept = default;

我们需要在头文件中声明析构函数和移动赋值运算符,但推迟定义,直到源文件知道完全定义的 B。请注意,我手动指定赋值运算符为 noexcept,因为如果我在声明时未将其设为 default,它将不会是 noexcept,即隐式声明的移动赋值运算符是。