BeigeAlert
BeigeAlert

Reputation: 313

C++ result of operator= changing after return *this

Here, I have a very simple program that moves a value from one object to another, making sure to remove the value from the one that it was taken from (leaving behind a '0').

#include <iostream>

struct S
{
    S(char val) : m_val(val) {}
    S& operator=(S&& other) noexcept
    {
        this->m_val = other.m_val;
        other.m_val = '0';

        return *this;
    }
    char m_val = '0';
};

int main()
{
    S a('a');
    S b('b');

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    a = std::move(b);

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    return 0;
}

As expected, the output of this program is:

a.m_val = 'a'
b.m_val = 'b'
a.m_val = 'b'
b.m_val = '0'

The value of 'b' is transferred from object b to object a, leaving a '0' behind. Now, if I generalize this a bit more with a template to (hopefully) automatically do the move and delete business, here's what I end up with... (distilled down of course).

#include <iostream>

template<typename T>
struct P
{
    P<T>& operator=(P<T>&& other) noexcept
    {
        T& thisDerived = static_cast<T&>(*this);
        T& otherDerived = static_cast<T&>(other);

        thisDerived = otherDerived;
        otherDerived.m_val = '0';

        return *this;
    }
protected:
    P<T>& operator=(const P<T>& other) = default;
};

struct S : public P<S>
{
    S(char val) : m_val(val) {}

    char m_val = '0';
};

int main()
{
    S a('a');
    S b('b');

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    a = std::move(b);

    std::cout << "a.m_val = '" << a.m_val << "'" << std::endl;
    std::cout << "b.m_val = '" << b.m_val << "'" << std::endl;

    return 0;
}

When run, the output is:

a.m_val = 'a'
b.m_val = 'b'
a.m_val = '0'
b.m_val = '0'

Uh oh! Somehow BOTH objects got "deleted". When I step through the body of the move assignment operator code... all seems well! a.m_val is 'b' like we expect... right up until the return *this; statement. Once it returns from the function, suddenly that value gets set back to '0'. Can anybody please shed some light on why this is happening?

Upvotes: 2

Views: 82

Answers (2)

songyuanyao
songyuanyao

Reputation: 172994

The problem is that S has a implicitly generated move assignment operator, which calls the move assignment operator of the base class (i.e. the P<T>::operator=), then perform member-wise move assignment on the members (i.e. S::m_val). In the P<T>::operator=, other.m_val has been assigned to '0', then back to S::operator= this->m_val is assigned by other.m_val and becomes '0' too.

You can define a user-defined move assignment operator for S and does nothing expect calling the base class version. e.g.

struct S : public P<S>
{
    S(char val) : m_val(val) {}

    char m_val = '0';

    S& operator=(S&& other) {
        P<S>::operator=(other);
        return *this;
    }
};

LIVE

Upvotes: 3

Sam Varshavchik
Sam Varshavchik

Reputation: 118445

P<T>& operator=(P<T>&& other) noexcept

This is an explicit move assignment operator for this template class.

struct S : public P<S> {

This subclass inherits from this template class. P<S> is its parent class.

This subclass does not have an explicit move assignment operator, so your C++ compiler helpfully creates a default move assignment operator for you, because that's how C++ works. The default move-assignment operator invokes the parent class's move assignment operator, and the default move assignment operator then move-assigns all members of this class.

Just because the parent class has an explicit move assignment operator (your move assignment operator) doesn't make this child class's default move assignment operator disappear. S's default move assignment operator is effectively this, very loosely speaking:

S &operator=(S &&other)
{
    P<S>::operator=(std::move(other));
    this->m_val=std::move(other.m_val);

    return *this;
}

That's what you get for free, from your C++ compiler. Isn't it nice of your C++ compiler to provide such a useful default move assignment operator for your class?

a = std::move(b);

This actually ends up invoking the above default move assignment operator.

Which first invokes the parent class's move assignment operator, the one you wrote.

Which effectively sets other.m_val to '0'.

And when it returns, this default move assignment operator also sets this->m_val to '0'.

Upvotes: 3

Related Questions