带有 pimpl 习语的移动操作

move operation with 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 定义的有效移动赋值吗?

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

move assignment operator 上的 cppreference:

Implicitly-declared move assignment operator

If no user-defined move assignment operators are provided for a class type (struct, class, or union), and all of the following is true:

  • there are no user-declared copy constructors;
  • there are no user-declared move constructors;
  • there are no user-declared copy assignment operators;
  • there are no user-declared destructors,

then the compiler will declare a move assignment operator as an inline public member of its class with the signature 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,这是隐式声明的移动赋值运算符将是。