Search code examples
c++unique-ptrmove-semanticspimpl-idiom

move operation with pimpl idiom


In the following code I am attempting to use a move assignment within the PIMPL idiom, but the code does not compile.

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;
}

While struct.cpp compiles with no warning, ```main.cpp`` does not, giving the error:

$ 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) ...

It is clear that the copy assignment A::operator=(const A&) is deleted because it is deleted for a std::unique_ptr. But why does the compiler attempt to use it in first place? Shouldn't std::move enforce the use of the move assignment, which is valid and defined for a std::unique_ptr?


Solution

  • While std::unique_ptr does have a move assignment operator and it certainly seems natural to want to make use of that fact to make A move-assignable, the user-declared constructor runs into problems.

    cppreference on the move assignment operator:

    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&&).

    Note the last bullet point: A has a user-declared destructor, so you don't get the implicitly-declared move assignment operator.

    If we want to make A move-assignable with a minimum of effort, we can explicitly declare the move assignment operator and request the default implementation as follows:

    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;
    

    We need to declare the destructor and move assignment operator in our header file but defer definition until the source file that's aware of the fully-defined B. Note that I manually specify that the assignment operator is noexcept, because if I don't make it default at point of declaration it won't be noexcept, which the implicitly-declared move assignment operator would be.