15

This is a bit of an inverse of all the "lvalue required as left operand of assignment" error questions.
I have a class that overloads operator[], but only the version that returns a temporary. If it were to return an int:

struct Foo
{
    int operator[]( int idx ) const { return int( 0 ); }
};

Foo f;
f[1] = 5;

I would rightfully get the lvalue compiler error. If it returns a struct type however, the compiler ( GCC 7.2 in this case ) doesn't complain at all:

struct Bar {};
struct Foo
{
    Bar operator[]( int idx ) const { return Bar(); }
};

Foo f;
f[1] = Bar();

Why wouldnt this complain in the same way if Bar is a temporary and it doesnt have an specialized operator =? Alternate question, is there some way to make this complain? Clearly this is a coding error if it were to be used this way

ByteMe95
  • 836
  • 1
  • 6
  • 18
  • 2
    The test case collapses to: `Bar() = Bar();` which compiles for me! I would hope for at-least one warning live: https://godbolt.org/z/_IPcrd – Richard Critten Jun 17 '19 at 22:11
  • Not sure if this is an option, but if you make the assignment operator of `Bar` private it will not compile. See also [Preventing copy construction and assignment of a return value reference](https://stackoverflow.com/q/3106224/2173773) – Håkon Hægland Jun 17 '19 at 22:18
  • Perhaps the struct has a default assignment operator that allows you to assign when doing `object[index] = Object()`? This seems fairly obvious to me.. https://imgur.com/cGk4RbU So I tested it.. turns out if you delete the assignment operator, it complains so.. seems confirmed to me.. – Brandon Jun 17 '19 at 22:20
  • Yes making the assignment operator private would prevent this scenario, but it would also prevent me from assigning when I do want to be able to assign – ByteMe95 Jun 18 '19 at 13:27

2 Answers2

17

is there some way to make this complain?

You can use an explicitly defaulted assignment operator with a ref-qualifier:

struct Bar {
    Bar& operator=(const Bar&) & = default;
//                             ^

This makes assignment of an rvalue ill-formed, while assignment of an lvalue remains well-formed.

Note that declaring the assignment operator disables implicit move assignment so you may need to define that as well, if needed (also as defaulted, and possibly with an rvalue ref qualifier, if appropriate).

Why wouldnt this complain in the same way if Bar is a temporary and it doesnt have an specialized operator =?

Because the implicitly generated assignment operators are not ref-qualified.

Clearly this is a coding error if it were to be used this way

Assignment of an rvalue is not universally an error. For some types that are supposed to behave like references, assignment of an rvalue is natural. That is so because the assignment modifies the referred object, and not the temporary object itself.

A typical use case is assigning to an rvalue std::tie (example from cppreference):

std::set<S> set_of_s; // S is LessThanComparable

S value{42, "Test", 3.14};
std::set<S>::iterator iter;
bool inserted;

// unpacks the return value of insert into iter and inserted
std::tie(iter, inserted) = set_of_s.insert(value);

Yes, it might be better if the implicit operators were qualified, and explicit declaration was required for non-qualified, considering referential types are exceptional rather than the norm. But that's not how the language is and changing it is a backwards incompatible change.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • 1
    I'd add that if `Bar` has members where move operations aren't the same as copy operations, you'll want to obey the Rule of Five since this declaration necessarily breaks the Rule of Zero. – aschepler Jun 17 '19 at 22:29
  • @aschepler added mention. Interestingly, this is a clear exception to rule of five, since you don't need to touch the destructor at all. – eerorika Jun 17 '19 at 22:43
  • The copy assignment operator also means there is no implicitly declared move constructor (and deprecates use of the implicitly declared copy constructor). – aschepler Jun 18 '19 at 02:29
1

Yes, there's a way to make this into a compile error by deleting these methods:

Bar& operator=(const Bar&)&& =delete;
Bar& operator=(Bar&&)&& =delete;

Just note that this will disable auto-generation of the other operators and constructors so you have to define them all:

struct Bar {
    Bar()=default;
    Bar(const Bar&) = default;
    Bar& operator=(const Bar&)&& =delete;
    Bar& operator=(Bar&&)&& =delete;
    Bar& operator=(const Bar&)& =default;
    Bar& operator=(Bar&&)& =default;
};
Quimby
  • 17,735
  • 4
  • 35
  • 55
  • If you have `default` ones, aren't the others implicitly deleted, and therefore you don't need the `delete` lines? – Mooing Duck Jun 17 '19 at 22:39
  • 1
    @MooingDuck Yes, but it also deletes move assignment and move constructors. Plus this clearly expresses the intent. Only setting `& =default;` works until someone decides that `&` is not needed or `&&` should be added. I would say that these qualifiers are not very well known because they are rare. Yes you can and should add a comment but then you might as well express it with code using `=delete`. – Quimby Jun 18 '19 at 06:57