Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What backward compatibility needs assignment for rvalues?

Tags:

c++

C++ Primer 5th:

(Code are also from the book, 99% context provided here)

#include <string>
using namespace std;
int main()
{
    //no error
    string s1 = "123", s2 = "aaaa";
    s1 + s2 = "wow";
    auto a = (s1 + s2).find("a");
}

Prior to the new standard (here it's saying C++11), there was no way to prevent such usage. In order to maintain backward compatability, the library classes continue to allow assignment to rvalues, However, we might want to prevent such usage in our own classes. In this case, we’d like to force the left-hand operand (i.e., the object to which this points) to be an lvalue.

What backward compatibility needs assignment for rvalues?

Btw, I am also curious about why s1 + s2 = "wow" is allowed but int i = 3, j = 4; i + j = 7; is not allowed. (Since it's closely related, I choose not to open another question)

like image 700
Rick Avatar asked Jul 18 '18 06:07

Rick


2 Answers

This might be an anti-climatic educated guess. I will welcome any other concrete example, however following general rules seems very reasonable.

  1. Such limitation would not break any specific code but it would restrict domain of accepted programs. c++ is rather conservative when it comes to such changes, sometimes to a great pain. A notable example would be most vexing parse, what code would break if A a(); was interpreted as default-constructed A? Yet it doesn't work this way to be backwards-compatible with c syntax. That's a rather PIA for syntax analysis.

  2. c++ allows semantic redefinition of operator meaning for user defined types. I don't know if there is a good example of semantic redefinition for operator= but there are some rather notable examples for other operators. boost::program_options "abuses" operator() in a quite weird way, to create a concise syntax, and Eigen redefines comma operator semantics. Pointer-to-member operators are often redefined to do something non-standard since they are not used by default often. So sometimes it's useful.

  3. I guess it might be useful with classes where operator= has side effects, and is not necessarily meant to change the value in memory. I imagine that in some embedded development you can have Row and Col and you write row * col = LED_ON or something along the lines. Another example from top of my head is an expression library, for example, there is no operator<=> yet so operator= could be used in it's place to evaluate stuff like (p ^ p) <=> p.

operator= is no special among operators and those are not really much more special than other member functions. If you write a code like this:

#include <iostream>
using namespace std;

struct A{
friend ostream& operator<<(ostream& out, A& a) { out << "A "; return out;}
};    

int main() {
    A a1, a2;
    cout << a1=a2 << '\n';
    return 0;
}

It will... break. That's because bitwise shift has precedence over =. It requires parentheses around a1=a2. This is to make a point that operator= really has no special rights in the language.

The other thing is that you can overload those operators nearly as you wish, so writing

#include <iostream>
using namespace std;

struct A{
    bool operator=(A& rhs) {return true;}
};    

int main() {
    A a1, a2;
    cout << (a1=a2) << '\n';
    return 0;
}

Is perfectly legal. Language provides operator with syntactic shortcuts and not much more. I don' think many complain that (a+b).method() works, this is the same for (a+b).operator=().

Bonus: The example with ints doesn't work because you cannot overload operators for primitive types and the default one is defined so it doesn't accept rvalues. It exhibits the behaviour that you seem to expect. Basically we are stripped from the freedom of redefining semantics of primitive type operators.

like image 124
luk32 Avatar answered Oct 17 '22 18:10

luk32


Here is an example of code that is currently perfectly valid (not even a warning with Clang) and that would break. The trick is that operator + returns a special object that has a redefined operator =.

In the example, the redefined operator = sets the first operand of the addition that created the special object:

#include <iostream>

// A simple class containing an int with a special operator =
class A {
public:
    int val;

    class B operator + (const A& other);
};

/* A very special subclass keeping a ref on the first member
 * of the addition of A objects that created it. This ref
 * will be assigned by operator = */
class B: public A {
    A& ref;      // ref on the A object used at creation time
    // private ctor: B can only be created from A objects sums
    B(A& orig): ref(orig) {
        this->val = orig.val;
    }

public:
    B(const B& src): A(src), ref(src.ref)  {}  // copy ctor...
    // second part of the trick: operator = assigns ref
    B& operator = (const A& src) {
        ref = src;
        return *this;
    }
    friend class A;
};

B A::operator +(const A& other) {
    B ret(*this);
    ret.val += other.val;
    return ret;
}

int main(){
    A a = {1}, b= {2}, c= {5};
    A d;

    d = a + b = c;
    // a+b will eval to a B having val=3 and ref=a
    // a+b = c will set a.val to 5 AFTER ABOVE EVALUATION
    // d = a+b will set d.val to 3

    std::cout << a.val << " " << b.val << " " << c.val << " " << d.val << std::endl;
    // outputs as expected: 5 2 5 3

    return 0;
}

Ok, I cannot imagine a real use case for that, but programmers can have strange ideas if the compiler accepts them (after all I could imagine that...). And I really think that letting operator + have side effects is an open door for operations like what I have shown.

like image 30
Serge Ballesta Avatar answered Oct 17 '22 19:10

Serge Ballesta