Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ assignment on type cast

Tags:

c++

casting

I stumbled upon something similar today, and subsequently tried a few things out and noticed that the following seems to be legal in G++:

struct A {
    int val_;
    A() { }
    A(int val) : val_(val) { }
    const A& operator=(int val) { val_ = val; return *this; }
    int get() { return val_; }
};

struct B : public A {
    A getA() { return (((A)*this) = 20); } // legal?
};

int main() {
    A a = 10;
    B b;
    A c = b.getA();
}

So B::getB returns a type A, after it as assigned the value 20 to itself (via the overloaded A::operator=).

After a few tests, it seems that it returns the correct value (c.get would return 20 as one may expect).

So I'm wondering, is this undefined behavior? If this is the case, what exactly makes it so? If not, what would be the advantages of such code?

like image 895
netcoder Avatar asked Oct 10 '22 04:10

netcoder


2 Answers

After careful examination, with the help of @Kerrek SB and @Aaron McDaid, the following:

return (((A)*this) = 20);

...is like shorthand (yet obscure) syntax for:

A a(*this); 
return a.operator=(20);

...or even better:

return A(*this) = 20;

...and is therefore defined behavior.

like image 174
2 revs Avatar answered Oct 17 '22 15:10

2 revs


There are a number of quite separate things going on here. The code is valid, however you have made an incorrect assumption in your question. You said

"B::getA returns [...] , after it as assigned the value 20 to itself"

(my emphasis) This is not correct. getA does not modify the object. To verify this, you can simply place const in the method signature. I'll then fully explain.

A getA() const {
    cout << this << " in getA() now" << endl;
    return (((A)*this) = 20);
}

So what is going on here? Looking at my sample code (I've copied my transcript to the end of this answer):

A a = 10;

This declares an A with the constructor. Pretty straightfoward. This next line:

B b; b.val_ = 15;

B doesn't have any constructors, so I have to write directly to its val_ member (inherited from A).

Before we consider the next line, A c = b.getA();, we must very carefully consider the simpler expression:

b.getA();

This does not modify b, although it might superfically look like it does.

At the end, my sample code prints out the b.val_ and you see that it equals 15 still. It has not changed to 20. c.val_ has changed to 20 of course.

Look inside getA and you see (((A)*this) = 20). Let's break this down:

this     // a pointer to the the variable 'b' in main(). It's of type B*
*this    // a reference to 'b'. Of type B&
(A)*this // this copies into a new object of type A.

It's worth pausing here. If this was (A&)*this, or even *((A*)this), then it would be a simpler line. But it's (A)*this and therefore this creates a new object of type A and copies the relevant slice from b into it.

(Extra: You might ask how it can copy the slice in. We have a B& reference and we wish to create a new A. By default, the compiler creates a copy constructor A :: A (const A&). The compiler can use this because a reference B& can be naturally cast to a const A&.)

In particular this != &((A)*this). This might be a surprise to you. (Extra: On the other hand this == &((A&)*this) usually (depending on whether there are virtual methods))

Now that we have this new object, we can look at

((A)*this) = 20

This puts the number into this new value. This statement does not affect this->val_.

It would be an error to change getA such that it returned A&. First off, the return value of operator= is const A&, and therefore you can't return it as a A&. But even if you had const A& as the return type, this would be a reference to a temporary local variable created inside getA. It is undefined to return such things.

Finally, we can see that c will take this copy that is returned by value from getA

A c = b.getA();

That is why the current code, where getA returns the copy by value, is safe and well-defined.

== The full program ==

#include <iostream>
using namespace std;
struct A {
    int val_;
    A() { }
    A(int val) : val_(val) { }
    const A& operator=(int val) {
        cout << this << " in operator= now" << endl; // prove the operator= happens on a different object (the copy)
        val_ = val;
        return *this;
    }
    int get() { return val_; }
};

struct B : public A {
    A getA() const {
        cout << this << " in getA() now" << endl; // the address of b
        return (((A)*this) = 20);
           // The preceding line does four things:
           // 1. Take the current object, *this
           // 2. Copy a slice of it into a new temporary object of type A
           // 3. Assign 20 to this temporary copy
           // 4. Return this by value
    } // legal? Yes
};

int main() {
    A a = 10;
    B b; b.val_ = 15;
    A c = b.getA();
    cout << b.get() << endl; // expect 15
    cout << c.get() << endl; // expect 20
    B* b2 = &b;
    A a2 = *b2;
    cout << b2->get() << endl; // expect 15
    cout << a2.get() << endl; // expect 15

}
like image 35
Aaron McDaid Avatar answered Oct 17 '22 13:10

Aaron McDaid