8

I was reading through the third edition of Effective C++ and I was surprised that a snippet on page 18 compiles. Here is a snippet inspired from that (compiler explorer link):

struct Rational {
    Rational(int numer, int denom): numer(numer), denom(denom) {}
    int numer, denom;
};

Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.numer * rhs.numer, lhs.denom * rhs.denom);
}

int main() {
    Rational a(1,2), b(3,4), c(5,6);
    a*b = c;
}

From my understanding, a*b is an r-value, and hence have no location in memory. How is it possible that an assignment operator can be called on an r-value?

eenz
  • 103
  • 1
  • 4
  • And if you wanted to prevent the assignment return a `const` value. – sweenish Dec 16 '20 at 18:14
  • Does this answer your question? [Operator overload which permits capturing with rvalue but not assigning to](https://stackoverflow.com/questions/5890382/operator-overload-which-permits-capturing-with-rvalue-but-not-assigning-to) – Richard Critten Dec 16 '20 at 18:15
  • 1
    @sweenish Doing so may interfere with move semantics. Unless recent language changes have fixed that? – François Andrieux Dec 16 '20 at 18:15
  • @FrançoisAndrieux, I don't know, but it is sometimes desirable to disallow further calls on the temporary until it "arrives" at it's object destination. If you know a better method, I'm all for it. – sweenish Dec 16 '20 at 18:20
  • @sweenish The only way you can call a member function on a temporary that is also going to be assigned to a destination object is to add member functions that return a reference to itself. So if you don't implement those, then it is already not possible to do that. – François Andrieux Dec 16 '20 at 18:22
  • @RichardCritten so is this happening because the default assignment operator can be called by both rvalues and lvalues? I noticed [this](https://godbolt.org/z/1YoWa6) code compiles, but [this](https://godbolt.org/z/qd3EM5) doesn't -- in the second case I'm following the advice of the link you sent to restrict the assignment operator only for lvalues. – eenz Dec 16 '20 at 18:26
  • @FrançoisAndrieux Not quite. You can call any function that returns the class type on a temporary. Return by reference is not a requirement. https://godbolt.org/z/KaWvjj – sweenish Dec 16 '20 at 18:33
  • @sweenish You're right, I misspoke. Though note that in your example `interfere` might as well be `static` and so does not quite illustrate the situation. Nonetheless it is only possible to "interfere" if the `class` provides these kinds of functions, which usually means that it is intended to be used in a chain. It would be very unusual for a class to provide such a function while also forbidding its use on an rvalue. But even in those cases, that is a property of the class and not of the function. In these rare cases you should provide an rvalue specified overload which you `delete`. – François Andrieux Dec 16 '20 at 19:07
  • 1
    @eenz I can't find the wording in the standard that explains why you can do as you've shown, but not for built-in types. The closest I found is that *"An rvalue can't be used as the left-hand operand of the **built-in** assignment or compound assignment operators."* and assigning to a `class` type uses that `class`' assignment operator rather than the built-in one. Though I am not entire sure about that line of reasoning. – François Andrieux Dec 16 '20 at 19:10
  • 1
    @eenz Please be aware that a value category is a property of an expression, not of an object. There are no rvalue and lvalue objects, rather there are rvalue and lvalue expressions. It is indeed not possible to take the address of an rvalue expression's result. But that object can still have an address. In the case of a `class` type return value, if you call a member function you can inspect `this` to determine its address. – François Andrieux Dec 16 '20 at 19:16
  • "rvalue" is an expression category , you are incorrectly conflating expressions and objects by making such statement as "r-value, and hence have no location in memory". All objects have a location in memory, whether or not the expression designating them is an rvalue expression. Imagine calling a member function that outputs `this` . – M.M Dec 16 '20 at 21:56

3 Answers3

3

a*b = c; calls the assignment operator on the Rational returned by a * b. The assignment operator generated is the same as if the following were defined:

Rational& Rational::operator=(const Rational&) = default;

There is no reason why this shouldn't be callable on a temporary Rational. You can also do:

Rational{1,2} = c;

If you wanted to force the assignment operator to be callable only on lvalues, you could declare it like this, with a & qualifier at the end:

Rational& operator=(const Rational&) &;
Artefacto
  • 96,375
  • 17
  • 202
  • 225
  • Thank you! This along with the distinction that "rvalue" is a category of expressions and not of objects as explained in the comments above really clears things up. – eenz Dec 17 '20 at 21:48
2

"a*b is an r-value, and hence have no location in memory" it is not quite right.

I add prints. The comments are the prints for each line of code

#include <iostream>
using namespace std;

struct Rational {
    
    Rational(int numer, int denom) : numer(numer), denom(denom) {
        cout << "object init with parameters\n";
    }

    Rational(const Rational& r)
    {
        this->denom = r.denom;
        this->numer = r.numer;
        cout << "object init with Rational\n";
    }
    ~Rational() {
        cout << "object destroy\n";
    }

    int numer, denom;
};

Rational operator*(const Rational& lhs, const Rational& rhs) {
    cout << "operator*\n";
    return Rational(lhs.numer * rhs.numer, lhs.denom * rhs.denom);
}

int main() {
    Rational a(1, 2), b(3, 4), c(5, 6); // 3x object init with parameters
    cout << "after a, b, c\n"; // after a, b, c
    Rational d = a * b = c; // operator*, object init with parameters, object init with Rational, object destroy

    cout << "end\n"; // end
    // 4x object destroy 
}

In the line Rational d = a * b = c; d is equal to c. This line call operator* function, that call the object init with parameters constructor. After that c object is copied to d object by calling copy constructor.

If you write the line: Rational d = a = c; // d == c // print only: object init with Rational the compiler assign the d object only to the last assign (object c)

jacob galam
  • 781
  • 9
  • 21
-2
Rational operator*(const Rational& lhs, const Rational& rhs) {
    cout << "operator*\n";
    return Rational(lhs.numer * rhs.numer, lhs.denom * rhs.denom);
}

operator *() returns an object of type Rational and that is the l value.

TKA
  • 183
  • 2
  • 4
  • This doesn't answer the question. They say in the question that `operator*` (or `a*b`) returns a r-value. The question is why the r-value can be assigned to. – Ted Klein Bergman Dec 16 '20 at 21:43
  • 1
    Even after the edit; the function returns by value so `a*b` is a r-value, not a l-value. I think you need to explain what you mean a bit more. – Ted Klein Bergman Dec 16 '20 at 21:47
  • as per jacob galam, the assignment operator is that way so whatever it returns can be further operated upon maintaining object chain of commands. a*b is a r-value but it returns an object of type Rational which is l-value. – TKA Dec 16 '20 at 22:04