The C++ implicit assignment operator is a non-ref-qualified member, even if the base class’s assignment has a ref-qualifier

Raymond Chen

Consider the following C++ class:

struct Base
{
    Base& operator=(Base const&) & = default;

    void BaseMethod();
};

This defines a class for which you can assign to an lvalue reference, but not to an rvalue reference.

extern Base GetBase();

Base b;
b = GetBase(); // allowed
GetBase() = b; // not allowed

Assigning to an rvalue is not generally useful, since the object has no name, and consequently it is difficult to do anything with the assigned-to object afterward.¹

Great, we got rid of assignment to a temporary, which we’ve seen has been a source of confusion.

Now consider this:

struct Derived : Base
{
};

Derived d;
Derived() = d; // is this allowed?

We created a derived class and inherited the assignment operator from it. Do you expect the inherited assignment operator to block rvalues?

You probably guessed that the answer is no, seeing as I gave it away in the title.

The reason is that I lied when I said that the assignment operator was inherited. It was not inherited. It was implicitly declared.

The rules for implicit declaration of the copy assignment operator are spelled out in [class.copy.assign], paragraphs 2 and 4. The short version is that a class is eligible for an implicitly-declared copy assignment operator if its base classes and non-static members all have a copy assignment operator. (Analogous rules apply for the implicitly-declared move assignment operator.)

The catch is that the implicitly-declared copy assignment and move assignment operators are declared as an unqualified assignment operator, regardless of the reference-qualifications of the base classes and members. In our example, we get

struct Derived : Base
{
    // compiler autogenerates these
    Derived& operator=(Derived const&) = default;
    //                                ^ no &
};

The lack of a ref-qualification means that this assignment operator applies equally to lvalues and rvalues.

Our attempt to block rvalue assignment fails to propagate to derived classes!

In order to repair this, each derived class must redeclare its assignment operator as lvalue-only.

struct Derived : Base
{
    Derived& operator=(Derived const&) & = default;
};

Oh, we’ve only started our journey down the rabbit-hole.

At least for now, explicitly declaring a copy assignment operator does not cause the implicitly-declared copy/move constructors to disappear, but the behavior is noted as deprecated in the C++ language specification, with the note that a future version of the language may indeed delete them.

Derived d;
Derived d2{ d }; // on borrowed time

To make sure you don’t run into trouble in the future, you’ll want to declare them explicitly.

struct Derived : Base
{
    Derived(Derived const&) = default;
    Derived(Derived&&) = default;
    Derived& operator=(Derived const&) & = default;
};

Great, we’ve restored the copy and move constructors.

But explicitly declaring any constructors causes us to lose the implicitly-declared default constructor.

Derived d; // doesn't work any more

We’ll have to bring that back too.

struct Derived : Base
{
    Derived() = default;
    Derived(Derived const&) = default;
    Derived(Derived&&) = default;
    Derived& operator=(Derived const&) & = default;
};

The same exercise applies if we also want to block the move assignment operator to rvalues, but it’s more urgent because an explicit declaration of a move assignment operator does delete both the copy and move constructors even in C++20.

struct Base
{
    Base& operator=(Base const&) & = default;
    Base& operator=(Base&&) & = default;

    void BaseMethod();
};

struct Derived : Base
{
    Derived() = default;
    Derived(Derived const&) = default;
    Derived(Derived&&) = default;
    Derived& operator=(Derived const&) & = default;
    Derived& operator=(Derived&&) & = default;
};

Phew, that was annoying.

¹ I mean, I guess you could do this:

Base b;

Something(GetBase() = b);
(GetBase() = b).BaseMethod();

but it seems pointless to go to the effort of asking Get­Base to create you a Base object, only to overwrite it with your own. You may as well just create your own temporary.

Something(Base(b));
Base(b).BaseMethod();

Or, if you didn’t even mean to create a temporary, just use the original value:

Something(b);
b.BaseMethod();

6 comments

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

  • Igor Tandetnik 0

    > Because explicitly declaring an assignment operator causes the implicitly-declared copy/move constructors to disappear.

    Are you sure about that? Seems to [work for me](https://godbolt.org/z/veb97xxrT). Also, if that were true, then `Derived(Derived const&) = default;` wouldn’t have worked either, as that requires `Base` to have a copy constructor, and you claim that it doesn’t have one.

    • Igor Tandetnik 0

      A nit: in many examples in the article, `Derived` isn’t actually derived from `Base`.

    • Raymond ChenMicrosoft employee 0

      You’re right. I confused it with the move assignment operator. I’ve revised the article. Thanks.

      • Igor Tandetnik 0

        There are still many instances where `Derived` is not actually derived from `Base`. And there’s still the issue that, in the world where `Derived` wouldn’t have an implicit copy constructor, `Base` wouldn’t have one either, and then `Derived(Derived const&) = default;` would default it as deleted.

  • Colin Stevens 0

    Sometimes C++ seems like … all a big mistake

  • Michaël Roynard 0

    > Great, we got rid of assignment to a temporary, which we’ve seen has been a source of confusion.

    Being able to do assignment into a temporary can be marginally useful. I can think on one use-case for it : let’s assume we have a matrix of bools whose underlying layout is like vector of bools. Then calling matrix(i, j) will yield a temporary (proxy) object whose assignment will result in mutating the corresponding bit in the matrix buffer.

    Well, we all know we should not imitate vector of bools but proxy objects are a thing, especially with the new addition of ranges in the standard. Being able to refine behavior for these cases is especially useful when implementing custom adapters/facades. And not being able to inherit such behavior will most probably be really annoying…

Feedback usabilla icon