5

I recently learned that member functions can be ref-qualified, which allows me to write

struct S {
    S& operator=(S const&) & // can only be used if the implicit object is an lvalue
    { 
      return *this; 
    }
};

S operator+(S const &, S const &) { 
  return {}; 
}

thereby preventing users from doing things like

S s{};
s + s = S{}; // error

However, I see that std::string's member operator= does not do this. So the following code compiles with no warnings

std::string s;
s + s = s;

Is there a reason for allowing this?

If not, would it be possible to add the ref-qualifier in the future, or would that break existing code somehow?

cigien
  • 57,834
  • 11
  • 73
  • 112
  • @cigien Why should not this operation s + s = s; be allowed? – Vlad from Moscow Apr 10 '20 at 14:07
  • 3
    @VladfromMoscow As far as I can tell, it doens't do anything useful. – cigien Apr 10 '20 at 14:08
  • Consider for example std::cout << ( s + s += 'a' ); – Vlad from Moscow Apr 10 '20 at 14:10
  • @VladfromMoscow They could just write `std::cout << ( s + s + 'a' );`. Not sure if I find that a compelling reason to allow it. – cigien Apr 10 '20 at 14:26
  • 1
    @VladfromMoscow: `+=` and `=` are different operators. The question is specifically about `operator=`, which overwrites the string. As such, there is no useful prvalue lhs version which would not be equivalent to just creating a prvalue from the `=` operand. – Nicol Bolas Apr 10 '20 at 14:51

2 Answers2

4

Likely, the timing plays a role in this decision. Ref-qualified member functions were added to the language with C++11, while std::string has been around since C++98. Changing the definition of something in the standard library is not something to be undertaken lightly, as it could needlessly break existing code. This is not a situation where one should exclusively look at why this weird assignment should be allowed (i.e. look for a use-case). Rather, one should also look at why this weird assignment should be disallowed (i.e. look at the benefits, and weigh them against the potential pain when otherwise working code breaks). How often would this change make a difference in realistic coding scenarios?

Looking at the comments, a counter to one proposed use-case was "They could just [another approach]." Yes, they could, but what if they didn't? Proposing alternatives is productive when initially designing the structure (std::string). However, once the structure is in wide use, you have to account for existing code that currently does not use the alternative. Is there enough benefit for the pain you could cause? Would this change actually catch mistakes often enough? How common are such mistakes in contexts where another warning would not be triggered? (As one example, using assignment instead of equality in the conditional of an if statement is likely to already generate a warning.) These are some of the questions with which the language designers grapple.

Please understand that I am not saying that the change cannot be done, only that it would need to be carefully considered.

JaMiT
  • 14,422
  • 4
  • 15
  • 31
  • I like your point about issuing warnings. Currently, this code triggers no warnings anywhere. Would it be possible to deprecate it for now, provide warnings for a few years, and then remove it entirely? – cigien Apr 10 '20 at 15:24
  • @cigien That might be possible. There is, though, the question of whether or not the effort is worth it. My guess (only a guess) is that it won't happen. – JaMiT Apr 10 '20 at 17:32
1

It cannot be certain why standard does not prohibit behaviour that you presented but there are a few possible explanations:

  1. It is simply an overlook in C++11. Before C++11 there was no ref-qualified methods and so someone has forgotten to change the bahaviour in standard.

  2. It is kept for backward compatibility for people who were using 'dirty' code like:

std::string("a") = "gds";

for some strange reasons.

And about adding this in future - it would be possible. But first the old operator= would have to become deprecated and later on removed because it would cause code like the one above to not compile. And even then some compilers would probably support it for backward standard compatibility

bartop
  • 9,971
  • 1
  • 23
  • 54
  • 1
    There's a third possibility: it just doesn't matter. C++ is not a nanny. It doesn't generally prohibit code that might be useful in odd corner cases. – Pete Becker Apr 10 '20 at 15:01
  • @PeteBecker That's what I thought. For the life of me, I can't think of any use-cases though. – cigien Apr 10 '20 at 15:06
  • @PeteBecker: It's more than that. I'd say it's a combination of the fact that these things are hard to do (you have to be trying to assign to a prvalue/xvalue) and, while they're not useful, they don't render your code undefined. Memory won't be lost or trashed, nobody will be accessing uninitialized objects, etc. – Nicol Bolas Apr 10 '20 at 15:09
  • Yeah, but it is somehow inconsistent with built-in types. Like, you can't do `(5 + 5) = 1` but it works with `std::string` – bartop Apr 10 '20 at 15:11
  • @bartop: Name a type in the standard library which is copy/move assignable yet is *not* assignable to a prvalue. – Nicol Bolas Apr 10 '20 at 15:15
  • That's kind of my question. I wanted to ask why *every* type in the STL doesn't behave like `int`, but that seemed a bit broad. (and there are probably some types that shouldn't) – cigien Apr 10 '20 at 15:18
  • @bartop your example is confusing. Why *shouldn't* that code be an error? At least I don't see what the user would expect that to do. – cigien Apr 10 '20 at 15:22
  • @bartop -- classes are, indeed, not like built-in types. `int f() { return 0; } f() = 3;` is illegal. `struct my_type {}; my_type g() { return my_type(); } g() = my_type();` is not. – Pete Becker Apr 10 '20 at 19:41