Search code examples
c++stringsubstringlvalue

Why is it not a compiler error to assign to the result of a substr call?


I just discovered the most baffling error and I don't understand why the compiler did not flag it for me. If I write the following:

string s = "abcdefghijkl";
cout << s << endl;

s.substr(2,3) = "foo";
s.substr(8,1) = '.';
s.substr(9,1) = 4;
cout << s << endl;

The compiler has no problem whatsoever with this, and the assignment statements appear to have no effect, based on what's printed out. In contrast,

s.front() = 'x';

has the effect I'd expect (since front returns a reference to a character) of changing the underlying string, and

s.length() = 4;

also has the expected effect of generating a compiler error complaining that you can't assign to something that isn't an lvalue, because length returns an integer. (Well, a size_t anyway.)

So... why on earth does the compiler not complain about assigning to the result of a substr call? It returns a string value, not a reference, so it shouldn't be assignable, right? But I've tried this in g++ (6.2.1) and clang++ (3.9.0), so it doesn't seem to be a bug, and it also doesn't seem to be sensitive to C++ version (tried 03, 11, 14).


Solution

  • The result of substr() is a std::string temporary object -- it's a self-contained copy of the substring, not a view on the original string.

    Being a std::string object, it has an assignment operator function, and your code invokes that function to modify the temporary object.

    This is a bit surprising -- modifying a temporary object and discarding the result usually indicates a logic error, so in general there are two ways that people try to improve the situation:

    1. Return a const object.
    2. Use lvalue ref-qualifier on assignment operator.

    Option 1 would cause a compilation error for your code, but it also restricts some valid use-cases (e.g. move-ing out of the return value -- you can't move out of a const string).

    Option 2 prevents the assignment operator being used unless the left-hand side is an lvalue. This is a good idea IMHO although not all agree; see this thread for discussion.

    In any case; when ref-qualifiers were added in C++11 it was proposed to go back and change the specification of all the containers from C++03, but this proposal was not accepted (presumably, in case it broke existing code).

    std::string was designed in the 1990s and made some design choices that seem poor today in hindsight, but we're stuck with it. You'll have to just understand the problem for std::string itself, and perhaps avoid it in your own classes by using ref-qualifiers, or views or whatever.