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
?
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
, orunion
), 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 signatureT& 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.