Logo

Blog


A destructor, =default, and the move operations

Today's post is a bit special in two ways. First, I continue to talk about move semantics, and this is the first time that I have the same topic for my monthly post and the monthly C++ Insights YouTube episode. Oh, spoiler alert :-)

Today's topic is a part of move semantic I often get questions about in my classes. This is, what happens to the move operations of a class with a user-declared destructor? I often learn that people believe that =default for the destructor is enough. We get all the special members back.

=default is enought, isn't it?

That thought is reasonable, as =default is more or less a way to tell the compiler to provide the default implementation for a certain member function.

Together with the destructors, this question usually comes up if the class in question serves as a base class. However, it is the same for derived classes.

Below is a piece of code that demonstrates the scenario.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Test {
  ~Test() = default;  A User-declared dtor
};

int main()
{
  Test t{};

  Test t2 = std::move(t);
}

In A, you can see the defaulted destructor. I left out the virtual part for simplicity reasons. This code compiles and runs fine. So this is the end of the post, =default, and all is good?

My type-trait tells me =default is enough

Well, we can look a bit deeper and very that we actually get a move and don't end up with a fallback copy. There is a type trait for this std::is_move_constructible_v. Sounds perfect, right?

1
2
3
4
5
6
struct Test {
  ~Test() = default;
};

B Verify move-ability with type-trait
static_assert(std::is_move_constructible_v<Test>);

The code compiles with the static_assert in B passing. So, this is the end of the post, right? That is the ultimate proof, Test is move constructible.

Actually, the answer is still no. The behavior of std::is_move_constructible_v is to check for move or copy! The type trait performs the same fallback as other move related code. It sounds it is time to fire up C++ Insights.

Your compiler knows the truth

If we put the initial example into C++ Insights, we can see the following transformed code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Test {
  inline ~Test() = default;
  // inline constexpr Test(const Test &) noexcept = default; C 
};

int main()
{
  Test t  = {};
  Test t2 = Test(static_cast<const Test&&>(std::move(t)));
  return 0;
}

Here you can see in C that the compiler only generates a copy constructor! But how does the resulting code look without a user-declared destructor?

Well, let's remove the user-declared destructor as shown below and transform this code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Test {
  //~Test() = default;
};

int main()
{
  Test t{};

  Test t2 = std::move(t);
}

The resulting code in C++ Insights is the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct Test {
  // inline constexpr Test(Test &&) noexcept = default; D 
};

int main()
{
  Test t  = {};
  Test t2 = Test(std::move(t));
  return 0;
}

This time, the difference is that we look at a move constructor in D.

The take away

Either don't tamper with the destructor at all or remember to default the move operations in case you like to keep them alive. Once you do that, remember that you now need to do the same for the copy operations.

Andreas