c++c++11move-semanticsrule-of-zero

just add destructor that do nothing can cause compile error (around std::move), why?


While I was learning std::move, I found a strange issue.

If I add only a destructor that do nothing to a perfect program, I will get a compile error.

#include <iostream>
using namespace std;

class M {
public:
  int database = 0;

  M &operator=(M &&other) {
    this->database = other.database;
    other.database = 0;
    return *this;
  }

  M(M &&other) { *this = std::move(other); }

  M(M &m) = default;
  M() = default;
  ~M() { /* free db */ }
};

class B {
public:
  M shouldMove;

  //~B(){}   //<---  ## Adding this line will cause compile error. ##
};

int main() {
  B b;
  B b2 = std::move(b); //## error at this line if the above line is added
  return 0;
}

Live code: https://ideone.com/UTR9ob

The error is invalid initialization of non-const reference of type 'B&' from an rvalue of type 'std::remove_reference<B&>::type {aka B}'.

Question:

I think the rule of zero is just a good practice.

However, from this example, it seems to me that it is a hard rule that if violated, I will get compile error.


Solution

  • The implicitly-declared move constructor is only present if a class does not have a user-declared destructor. Therefore the answer to 2. is YES.

    The answer to 1. is that this is hard rule and can be found in 12.8, paragraph 9 of the standard:

    If the definition of a class X does not explicitly declare a move constructor, 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,
    • X does not have a user-declared destructor, and
    • the move constructor would not be implicitly defined as deleted.

    [ 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 ]

    The best way of getting this to run,is by using something like a smart pointer, i.e., a base class or member that does define all five special members (and very little else) so that you don't have to. In this case, an integer handle equivalent to std::unique_pointer should work well. However, keep in mind that databases, like files, can have errors while closing, so standard non-throwing destructor semantics don't cover all cases.