Reputation: 313
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
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;
}
};
Upvotes: 3
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