29 Aug, 2016

Episode Eleven: To Kill a Move Constructor

Unlike copy operations, which are provided by the compiler if not user declared, move operations can and often are suppressed such that a class might not have one. Furthermore, it is possible for a class to have a —user declared— move operation which is both defined as deleted, and at the same time ignored by overload resolution, as if it didn't exist.

On Copy and Move Operations

Copy and move are fundamental operations of the C++ language:

12.8 [class.copy]/11 A class object can be copied or moved in two ways: by initialization (12.1, 8.6), including for function argument passing (5.2.2) and for function value return (6.6.3); and by assignment (5.18). Conceptually, these two operations are implemented by a copy/move constructor (12.1) and copy/move assignment operator (13.5.3).

A class always has a copy constructor and an assignment operator. The compiler will implicitly declare either (or both) when they are not user declared, and while they might be —implicitly— defined as deleted, they are always present:

12.8 [class.copy]/7 If the class definition does not explicitly declare a copy constructor, a non-explicit one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted; otherwise, it is defined as defaulted (8.4). [...]

12.8 [class.copy]/18 If the class definition does not explicitly declare a copy assignment operator, one is declared implicitly. If the class definition declares a move constructor or move assignment operator, the implicitly declared copy assignment operator is defined as deleted; otherwise, it is defined as defaulted (8.4). [...]

A class might not have a move constructor or move assignment operator. The compiler will only implicitly declare them when certain conditions are met, and even then they might still be —implicitly— defined as deleted:

12.8 [class.copy]/9 If the definition of a class X does not explicitly declare a move constructor, a non-explicit one will be implicitly declared as defaulted if and only if

  • X does not have a user-declared copy constructor,
  • X does not have a user-declared copy assignment operator,
  • X does not have a user-declared move assignment operator, and
  • X does not have a user-declared destructor.

[Note: When the move constructor is not implicitly declared or explicitly supplied, expressions that otherwise would have invoked the move constructor may instead invoke a copy constructor. —end note]

12.8 [class.copy]/20 If the definition of a class X does not explicitly declare a move assignment operator, one will be implicitly declared as defaulted if and only if

  • X does not have a user-declared copy constructor,
  • X does not have a user-declared move constructor,
  • X does not have a user-declared copy assignment operator, and
  • X does not have a user-declared destructor.

Declaring a copy/move operation as deleted yields the following four combinations:

struct copyable { // and movable
  copyable() = default;
  copyable(copyable const&) { /*...*/ };
  copyable& operator=(copyable const&) { /*...*/ return *this; }
};

struct movable_only {
  movable_only() = default;
  movable_only(movable_only const&) = delete; // redundant
  movable_only(movable_only&&) { /*...*/ };
  movable_only& operator=(movable_only const&) = delete; // redundant
  movable_only& operator=(movable_only&&) { /*...*/ return *this; }
};

struct copyable_only {
  copyable_only() = default;
  copyable_only(copyable_only const&) { /*...*/ };
  copyable_only(copyable_only&&) = delete; // discouraged
  copyable_only& operator=(copyable_only const&) { /*...*/ return *this; }
  copyable_only& operator=(copyable_only&&) = delete; // discouraged
};

struct non_copyable { // nor movable
  non_copyable() = default;
  non_copyable(non_copyable const&) = delete;
  non_copyable(non_copyable&&) = delete; // redundant
  non_copyable& operator=(non_copyable const&) = delete;
  non_copyable& operator=(non_copyable&&) = delete; // redundant
};

[Note: Unusual cv-qualifications, like those of a mutating copy constructor, are deemed non-idiomatic and thus ignored. -end note]

No special case is made for a type that defines both copy and move operations; a copy constructor/assignment operator is a viable match for a move construction/assignment operation, given that X&& binds to X const& —thus copyable is movable too—. Consider:

struct copyable_and_movable {
  copyable_and_movable() = default;
  copyable_and_movable(copyable_and_movable const&) { /*...*/ };
  copyable_and_movable(copyable_and_movable&&) { /*...*/ };
  copyable_and_movable& operator=(copyable_and_movable const&) { /*...*/ return *this; };
  copyable_and_movable& operator=(copyable_and_movable&&) { /*...*/ return *this; };
};

The types copyable and copyable_and_movable are not fundamentally different, both are copyable and movable. The only distinction is which candidate constructor gets selected by overload resolution to perform each operation.

The Odd One Out

A copyable-only type is a weird beast of contradictory nature; it's copyable, and since copy is a valid form of move, in principle it should be movable too. Explicitly defining move operations as deleted, instead of letting them decay to copies, goes against the very notion of moves as an optimization of copies.

Little to no attention is paid to copyable-only types in the standard library, whose CopyConstructible and CopyAssignable semantic requirements include those of MoveConstructible and MoveAssignable respectively. The core language does not care much for them either, and it will not propagate such oxymoron further, when given a chance it will produce a copyable type over a copyable-only one:

struct foo {
  copyable_only _;
  foo() = default;
  foo(foo const&) = default;
  foo(foo&&) = default; // defaulted as deleted
  foo& operator=(foo const&) = default;
  foo& operator=(foo&&) = default; // defaulted as deleted
};
static_assert(std::is_copy_constructible<foo>::value == true); // holds
static_assert(std::is_move_constructible<foo>::value == false); // fires! moves decay to copies

Only move operations that are explicitly defined as deleted lead to copyable-only types. A move operation that is implicitly defined as deleted —or explicitly defaulted as deleted— behaves as if it didn't exist at all.

12.8 [class.copy]/11 [...] A defaulted copy/move constructor for a class X is defined as deleted (8.4.3) if X has:

  • a variant member with a non-trivial corresponding constructor and X is a union-like class,
  • a potentially constructed subobject type M (or array thereof) that cannot be copied/moved because overload resolution (13.3), as applied to M's corresponding constructor, results in an ambiguity or a function that is deleted or inaccessible from the defaulted constructor,
  • any potentially constructed subobject of a type with a destructor that is deleted or inaccessible from the defaulted constructor, or,
  • for the copy constructor, a non-static data member of rvalue reference type.

A defaulted move constructor that is defined as deleted is ignored by overload resolution (13.3, 13.4). [Note: A deleted move constructor would otherwise interfere with initialization from an rvalue which can use the copy constructor instead. -end note].

12.8 [class.copy]/23 A defaulted copy/move assignment operator for class X is defined as deleted if

  • a variant member with a non-trivial corresponding assignment operator and X is a union-like class, or
  • a non-static data member of const non-class type (or array thereof), or
  • a non-static data member of reference type, or
  • a direct non-static data member of class type M (or array thereof) or a direct base class M that cannot be copied/moved because overload resolution (13.3), as applied to M's corresponding assignment operator, results in an ambiguity or a function that is deleted or inaccessible from the defaulted assignment operator.

A defaulted move assignment operator that is defined as deleted is ignored by overload resolution (13.3, 13.4).

This special property can be extended further to arbitrary conditions, with some help from a utility class with conditionally deleted move operations:

template <bool Condition>
struct _enable_moves_if;

template <>
struct _enable_moves_if<true> {
  _enable_moves_if() = default;
  _enable_moves_if(_enable_moves_if const&) = default;
  _enable_moves_if& operator=(_enable_moves_if const&) = default;

  _enable_moves_if(_enable_moves_if&&) = default;
  _enable_moves_if& operator=(_enable_moves_if&&) = default;
};

template <>
struct _enable_moves_if<false> {
  _enable_moves_if() = default;
  _enable_moves_if(_enable_moves_if const&) = default;
  _enable_moves_if& operator=(_enable_moves_if const&) = default;

  _enable_moves_if(_enable_moves_if&&) = delete;
  _enable_moves_if& operator=(_enable_moves_if&&) = delete;
};

It is then possible to have move operations conditionally participate in overload resolution by deferring to a base (that performs the actual move operation), and the _enable_moves_if<Condition> helper class:

template <typename T>
class _bar_impl {
public:
  _bar_impl() = default;
  _bar_impl(_bar_impl const&) { /*...*/ };
  _bar_impl(_bar_impl&&) {
    // only instantiated if is_xxx<T>:value is true
    static_assert(is_xxx<T>::value);
    /*...*/
  }
  /*...*/
};

template <typename T>
class bar : _bar_impl<T>, _enable_moves_if<is_xxx<T>::value>
{
public:
  bar() = default;
  bar(bar const&) = default;
  bar(bar&&) = default; // ignored if is_xxx<T>::value is false
  /*...*/
};

bar<xxx> bx1, bx2(std::move(b1)); // bx1: default, bx2: move
bar<not_xxx> bnx1, bnx2(std::move(bnx1)); // bnx1: default, bnx2: copy

True Story: A wild copyable-only type appears

Consider the following definition of non_empty_string, a wrapper that enforces the invariant that the wrapped string never be empty:

class non_empty_string {
  std::string _value;

public:
  explicit non_empty_string(std::string value)
    : _value(std::move(value))
  {
    if (_value.empty())
      throw std::invalid_argument("value is empty");
  }
  /*...*/
};

By saying nothing, this class will have implicit copy and move operations. The copy operations will do exactly what it is needed, but the move operations would leave the moved-from string empty —it's actually unspecified, empty is a possible valid but unspecified state, and a reasonable one to expect—, breaking the class' invariants. The implicit move operations generated by the compiler are not acceptable —a consequence of non-destructive moves—, and must be suppressed; and given that the rules for implicit definitions of special functions aren't exactly trivial, and that explicit is said to be better than implicit, deleting them would seem reasonable:

class non_empty_string {
  /*...*/
  non_empty_string(non_empty_string const&) = default;
  non_empty_string(non_empty_string&&) = delete;
  /*...*/
};

Lo and behold, as a copyable-only —oxymoronic— type is born, one that can be copied but not moved. It ought to be possible, however, to move non_empty_strings without breaking their invariants, it simply won't be more efficient than copying them. That can be accomplished implicitly:

class non_empty_string {
  /*...*/
  non_empty_string(non_empty_string const&) = default;
  /*...*/
};

...or explicitly:

class non_empty_string {
  /*...*/
  non_empty_string(non_empty_string const&) = default;
  non_empty_string(non_empty_string&& other) : non_empty_string(other) {}
  /*...*/
};

One Meddling Constructor

While a constructor template is never used to generate a copy/move constructor, they are candidates and may end up being selected by overload resolution to perform copy/move construction. It is somewhat known that an unconstrained forwarding constructor can, in certain scenarios, answer to what it seemingly is a call to a copy constructor:

template <typename T>
class wrapper {
  T _value;
public:
  wrapper() = default;
  wrapper(wrapper const&) = default;
  wrapper(wrapper&&) = default;

  template <typename U>
  explicit wrapper(U&& value)
  : _value(std::forward<U>(value))
  {}
};

wrapper<noncopyable> w;
wrapper<noncopyable> const wc;

wrapper<noncopyable> cc(w); // (1)
wrapper<noncopyable> ccc(wc); // (2)

Only one of the above calls a copy constructor, the other call is handled by the unconstrained forwarding constructor:

  • (1) wrapper(U&&) with U = wrapper<noncopyable>& is a better match than the copy constructor, since it's an exact match;

  • (2) wrapper(wrapper const&), the copy constructor, is an exact match;

Copy construction is the direct-initialization of an object of some type with an lvalue of the same (possibly cv-qualified) type. The selected constructor to perform such initialization is usually a copy constructor, but an unconstrained forwarding constructor will be selected when the cv-qualifications are not an exact match for the copy constructor.

What is more surprising —though it shouldn't in the context of this article—is that an unconstrained forwarding constructor can answer to what appears to be a move constructor call, even when there is a move constructor explicitly defined:

wrapper<noncopyable> mc(std::move(w)); // (3)
wrapper<noncopyable> mcc(std::move(wc)); // (4)

None of the above calls a copy or move constructor, both calls are handled by the unconstrained forwarding constructor:

  • (3) wrapper(U&&) with U = wrapper<noncopyable> is an exact match, the move constructor is completely ignored by overload resolution;

  • (4) wrapper(U&&) with U = wrapper<noncopyable> const would be a better match than the move constructor even if it were not ignored, since it's an exact match.

Move construction is the direct-initialization of an object of some type with an rvalue of the same (possibly cv-qualified) type. The selected constructor to perform such initialization is usually a move constructor if one exists, or a copy constructor otherwise, but an unconstrained forwarding constructor will be selected when the move constructor is ignored by overload resolution, or when the cv-qualifications are not an exact match for the move constructor.

Overloading on forwarding references must be exercised with caution. In the case of forwarding constructors, they better be constrained to not overthrow a copy/move constructor:

template <typename T>
class wrapper {
  /*...*/
  template <typename U, typename Enable = typename std::enable_if<
    !std::is_same<std::decay_t<U>, T>::value
  >::type>
  explicit wrapper(U&& value)
  : _value(std::forward<U>(value))
  {}
};

Summary

Copy and move are fundamental operations in the C++ lands. The rules for implicitly declared copy/move constructors and assignment operators are complex, and it is better to be explicit... except when it isn't!

  • A class always has a copy constructor/assignment operator, if one is not explicitly declared it will be implicitly provided by the compiler. An implicitly declared copy constructor/assignment operator will be defined as deleted if the class declares a move operation, otherwise it will be defined as defaulted.
  • A class might not have a move constructor/assignment operator, if one is not explicitly declared it will only be implicitly provided by the compiler when the class declares no copy nor move operations, nor a destructor. An implicitly declared move constructor/assignment operator will be defined as defaulted.
  • A defaulted copy/move constructor or assignment operator will be defined as deleted when, amongst other conditions, the corresponding operation is not available for a base class or member subobject. A defaulted move constructor/assignment operator that is defined as deleted is ignored by overload resolution.
  • Move construction/assignment of an object of a type without a corresponding member, or whose corresponding member is ignored by overload resolution, results in a copy operation instead.
  • Never declare a move constructor or assignment operator as deleted, copyable-only types are syntactically valid but make no sense semantically.
  • Copy/move construction might not involve a copy/move constructor, terminology can be a cruel mistress sometimes...

References: