June 12th, 2023

The move constructor that you have to declare, even though you don’t want anyone to actually call it

Blah blah blah C++ return value optimization (RVO), named return value optimization (NRVO), and copy elision.

RVO support was optional in C++11 but became mandatory in C++17. NRVO support remains optional (but recommended).

To allow NRVO in C++17 (or RVO and NRVO in C++11), a move constructor must be available, even though the compiler will not call it if the optimization is employed.

You may have a class that you want to participate in RVO or NRVO, but you also don’t want it to be moved. For example, it may contain a std::mutex, which is not movable. But you nevertheless have to declare a move constructor. What can you do?

Declare the move constructor, but don’t implement it.

class MyClass
{
public:
  MyClass();

  // Not copyable.
  MyClass(const MyClass&) = delete;

  // Movable only for NRVO purposes (and RVO in C++11).
  // Never implemented.
  MyClass(MyClass&&);

  // Not assignable.
  void operator=(const MyClass&) = delete;
};

MyClass test1()
{
    return MyClass(); // RVO
}

MyClass test2()
{
    MyClass c;
    return c; // NRVO
}

MyClass test3()
{
    MyClass c, d;
    if (some_condition()) {
        return c; // failed NRVO
    } else {
        return d; // failed NRVO
    }
}

We declared a move constructor in order to permit RVO in C++11 and NRVO everywhere. The compiler demands to see a move constructor, even though RVO and NRVO ultimately optimize it out.

The first test shows that RVO works. The second test shows that NRVO works.

The test3 version compiles but does not link: NRVO is not possible given the way that test3 is written, and the compiler is forced to use the move constructor to move either c or d into the return value. We declared the move constructor but never implemented it, so we get an unresolved external, which tells us, “Sorry, this object doesn’t support move, even though the tin says that it does. The label on the tin is a lie and exists only to allow the compiler to use RVO and NRVO.”

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • Pados Károly · Edited

    > You may have a class that you want to participate in RVO or NRVO, but you also don’t want it to be moved. For example, it may contain a std::mutex, which is not movable. But you nevertheless have to declare a move constructor.

    I think this needs a little correction. RVO since C++17 (e.g. test1 above) does not require a move constructor to be declared. It is only required for NRVO. You actually say this...

    Read more
    • Pados Károly · Edited

      Some extra info for the uninitiated: The rules actually have to do with mandatory vs optional copy elision. RVO and NRVO are just specific cases for each, but there are other cases too where copy elision will or could happen. Whether it is mandatory or not dictates if a move constructor must be present.

  • Sukru Tikves · Edited

    I would expect the diagnostics message would not be very helpful in this context.

    Actually... let me try...

    <code>

    Yes, you need to know what exactly is going on, otherwise one would not recognize failed RVO.

    Read more
    • Daniel Sturm

      Damn and that’s clang. I don’t even want to imagine what eldritch horrors gcc or msvc generate.

      C++ error messages are in an awful state and it’s impressive with what we put up with.