What is the meaning of assigning to return value of getter taking this pointer by value?

116 views Asked by At

This doubt came to me when I jumped on an existing code and mistakenly used a getter to set a property,

obj.getProp() = otherProp;

instead of calling the setter,

obj.setProp(otherProp);

I did not realize the mistake because there was no error at compilation or runtime; the assignment resulted in a no-op.

So I came up with the following example, which outputs 337:

#include <iostream>

struct A {
    int x = 0;
    A(int x) : x(x) {}
    A(A& a) : x(a.x) {}
    void operator=(A const& other) { x = other.x; }
};

struct B {
    A a{3};
    int x{3};
    A  getAbyVal() { return a; }
    A& getAbyRef() { return a; }
    int getXbyVal() { return x; }
};

int main() {
    B b;
    std::cout << b.a.x;   // this and the other two cout print what I expect, but...
    b.getAbyVal() = A{7}; // ... I expected this to fail at compilation time in the first place...
    //b.getXbyVal() = 3;  // ... just like this fails.
    std::cout << b.a.x;
    b.getAbyRef() = A{7};
    std::cout << b.a.x;

}

So my question is two folds:

  • what in b.getAbyVal() = A{7}; is different from b.getXbyVal() = 3; so that the former compiles and the latter doesn't (beside the fact that the types are A and int)?
  • changing void operator=(A const& other) { x = other.x; } to void operator=(A const& other) & { x = other.x; } makes b.getAbyVal() = A{7}; fail to compile. Why is this the case?
2

There are 2 answers

1
SergeyA On BEST ANSWER

what in b.getAbyVal() = A{7}; is different from b.getXbyVal() = 3; so that the former compiles and the latter doesn't (beside the fact that the types are A and int)?

Surprisingly, the difference in types is exactly what makes one compile correctly, and other to fail.

A has an assignment operator defined for it, so compiler dutifully invokes it on the return value (only to discard the whole object later). But the code you wrote supports this. From the compiler view, some other interesting things might have happened in your assignment operator, despite the fact that the object will be eradicated (side effects in formal parlance).

With int as a return value, compiler knows there are no side effects of assigning value to int, so assigning any value to object which is to be eradicated immediately does not make any sense.

2
cigien On

what in b.getAbyVal() = A{7}; is different from b.getXbyVal() = 3; so that the former compiles and the latter doesn't (beside the fact that the types are A and int)?

The difference is precisely that one functions returns a class type, and the other function returns a POD type. A temporary int, for example can't be assigned to:

42 = x; // error

so similarly the language disallows assigning to a temporary int returned from a function as well. This is not the default behavior for user-defined class types so assigning to a temporary A compiles:

A{} = x; // ok

changing void operator=(A const& other) { x = other.x; } to void operator=(A const& other) & { x = other.x; } makes b.getAbyVal() = A{7}; fail to compile. Why is this the case?

Adding a & at the end of is called a ref-qualifier, and allows a user-defined class to have the same semantics as a POD type when it comes to assigning to a temporary. Adding a & at the end of operator= constrains it to only be used on an l-value (basically, a named variable, or a reference returned from a function).

A{} = x;  // now error
A a;
a = x;    // still ok