C++ class instances returned by value not acting like rvalues

Viewed 103

I had an interesting typo in some code the other day which led to a lengthy and frustrating debugging session, before I finally noticed the stray character on a much earlier line. The issue was that I had a stray '-' in my code, which the compiler was turning into a call to unary .operator-() on a member variable many lines further down in the code (below some mid-function comment blocks), and was then performing an assignment into the temporary variable holding the result of that operator-() call, which I had intended to actually go into the member variable itself. Net result: it was as if the assignment wasn't being performed at all, because it was silently being stored into a temporary.

I've simplified the issue into a minimum viable code demonstration, here, which generates no compile warnings or errors on either modern gcc or modern clang (but is untested in MSVC):

#include <stdio.h>

class foo
{
public:

    int x;

    foo(): x(0) {}
    foo( int x_in ): x(x_in) {}
    foo operator-() const { return foo(-x); }
};

int main( int argc, char** argv )
{
    foo a( 10 );
    foo b( 20 );

    a = b;
    printf( "a: %d\n", a.x ); // a: 20

    a = foo( 10 );

    {
    }- // oops

    a = b;  // assigns the value '20' into the temporary storage holding '-a'
    printf( "a: %d\n", a.x ); // a: 10

    return 0;
}

My feeling is that the parsed '-a' should be treated as an rvalue, as it would be if 'a' was defined as an int rather than as a class, and the attempted assignment should generate a compile error, but it doesn't seem to do that in practice.

My first thought about how to guard against this was to change the function signature from foo operator-() const to const foo operator-() const, so at least compilers will complain if I try to assign a value to a generated temporary again. Does anybody have a better solution, here, or an argument that it's not an issue I should be guarding against?

2 Answers

if 'a' was defined as an int rather than as a class, and the attempted assignment should generate a compile error

Class types behave differently with build-in types in this case, the copy assignment operator is allowed to be called on the temporary object here.

As you said, you can change the return type of operator- to const foo. Or you can qualify the copy assignment operator (and move assignment operator if necessary) with lvalue-reference (since C++11) to prevent assignment on rvalues.

class foo
{
public:

    int x;

    foo(): x(0) {}
    foo( int x_in ): x(x_in) {}
    foo operator-() const { return foo(-x); }

    foo& operator=(const foo&) & = default;
};

If you don't declare any copy/move constructor/assignment operator, they will be implicitly provided to you by the compiler in the form:

T::T(const T&);
T::T(T&&);
T& T::operator=(const T&);
T& T::operator=(T&&);

Howard Hinnant has an excellent slide presentation, which explains the rules. It's called Everything You Ever Wanted to Know About Move Semantics. Slide 30 has the final result.

In your example, these operators will work on both l- and r-value references, which is why it compiles with no errors.

To avoid assignment to r-values, you can declare your own (defaulted) versions with l-value ref-qualifier, thus preventing the compiler from providing implicitly declared ones.

So, if your case it could be something like this:

class foo
{
public:

    ...

    foo& operator=(const foo&) & = default;
    foo& operator=(foo&&) & = default;
};

You shouldn't have to make any other changes to your code.

Full example on coliru.

Look ma, no assignment.

Related