-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Description
Given the assignment operator typically (and is encouraged to) return T&
, this creates a non-obvious danger (if admittedly rare to encounter), and subtly breaks from the principle of "do as the ints do" mentioned in that guideline. Given a typical class definition:
class Foo {
// ....
Foo& operator=(const Foo &rhs);
};
without a ref qualifier, the operator=
method is allowed to bind to both lvalue and rvalue objects, which enables the following to work:
Foo somefoo;
Foo{} = somefoo;
This not only looks weird and breaks the "do as the ints do" principle, as this doesn't work:
int someint{};
int{} = someint; // error: expression is not assignable
but because operator=
returns an lvalue reference to *this
, the rvalue-ness of the object is lost, which arms a trap:
auto&& anotherfoo = Foo{} = somefoo; // !! dangling lvalue ref !!
For its part, Clang warns on this with -Wdangling
, but neither GCC (with -Wall -Wextra
) or MSVC (with /W4
) seem to issue a warning. However, both of these issues are solved by simply using an lvalue ref specifier:
Foo& operator=(const Foo &rhs) &;
Now you can't assign to a temporary, just like an int:
Foo somefoo;
Foo{} = somefoo; // error: no viable overloaded '='
// note: candidate function not viable: expects an lvalue for object argument
which in turn prevents the rvalue from being converted to an lvalue and assignable to an lvalue reference. And these are errors which must be addressed, rather than mere warnings that can be ignored. As a bonus, it also works fine with defaulting:
Foo& operator=(const Foo &rhs) & = default;
so you can retain default copy assignment semantics with the extra protection. I understand that there can be times where assigning to a temporary can make sense, for instance (just something off the top of my head) if you create a class that acts as a kind of tunnel to an external resource and assigning is a method of writing data:
ExternalPort{"127.0.0.1:1234"} = std::array{0, 1, 2, 3, 4};
But I wager this kind of use is rather rare, and you'd know when you need to do it, so it would make sense to consider making the assignment operator be lvalue-ref-qualified by default unless you have a specific need not to.
More broadly, it might be worth considering this anywhere a class method returns *this
as a T&
. But at least with stuff like +=
and *=
, I can see a slightly stronger argument for composability:
std::string message = part1 + part2 + part3 + part4;
That creates 3 temporary strings, while this:
std::string message = part1 += part2 += part3 += part4;
creates just 1. Of course, that can be reworked, either splitting it into multiple lines or using std::format
, so the need still isn't terribly strong.
In any case, that's been bubbling in my mind for a bit now, and got the drive to make the suggestion.