class A
{
std::string val1;
A(std::string str) : val1(std::move(str)){}
};
class B: A
{
B(std::string str) : A(str){}
};
In this case, str
would be copied twice, or not?
What is the best way to use Copy & Move idiom with children's classes?
CodePudding user response:
In cases like this, the best way to be sure is often to log copies and moves. Simply wrap std::string
into a custom class or struct with custom copy and move semantics, like this:
class StringWrapper
{
public:
std::string data;
StringWrapper(const char* cstr)
: data(cstr) {}
// Copy constructor and assignment
StringWrapper(const StringWrapper& other)
: data(other.data)
{
std::cout << "Copy constructor!\n";
}
StringWrapper& operator=(const StringWrapper& other)
{
data = other.data;
std::cout << "Copy assignment!\n";
return *this;
}
// Move constructor and assignment
StringWrapper(StringWrapper&& other)
: data(std::move(other.data))
{
std::cout << "Move constructor!\n";
}
StringWrapper& operator=(StringWrapper&& other)
{
data = std::move(other.data);
std::cout << "Move assignment!\n";
return *this;
}
};
Then to replace every use of std::string
by StringWrapper
:
class A
{
public: // Public for testing
StringWrapper val1;
A(StringWrapper str) : val1(std::move(str)){}
};
class B: public A // Private inheritance should be avoided
{
public: // Public for testing
B(StringWrapper str) : A(std::move(str)){}
};
With a small test:
int main()
{
StringWrapper strA("hello");
A a(strA);
std::cout << a.val1.data << '\n';
StringWrapper strB("world");
B b(strB);
std::cout << b.val1.data << '\n';
}
Then to analyze the output:
Copy constructor!
Move constructor!
hello
Copy constructor!
Copy constructor!
Move constructor!
world
The first copy happens when passing strA
by value. It is necessary since we don't want changes to strA
to affect a.val1
.
The second copy is very similar: it occurs when passing strB
by value. It is also necessary for the same reason.
The third copy, on the other hand, is redundant. It occurs when passing str
to the parent constructor in B
's constructor. str
will be destroyed at the end of the construction, so we better move its content than copy it:
class B: public A
{
public:
B(StringWrapper str) : A(std::move(str)){} // Replaced copy with move
};
Fixed!
Copy constructor!
Move constructor!
hello
Copy constructor!
Move constructor!
Move constructor!
world
CodePudding user response:
If you are new to C , it is maybe a good idea to simply investigate what is happening if you execute your code instead of take maybe wrong assumptions.
To see what is happening, simply replace std::string
with your own class and put some debug output in it.
struct mystring
{
mystring() { std::cout << "Default" << std::endl; }
mystring( mystring&& ) { std::cout << "Move" << std::endl; }
mystring( const mystring& ) { std::cout << "Copy" << std::endl; }
mystring& operator=( const mystring& ) { std::cout << "Copy assign" << std::endl; return *this; }
mystring& operator=( mystring&& ) { std::cout << "Move assign" << std::endl; return *this; }
~mystring() { std::cout << "Delete" << std::endl; }
};
And with some test code
int main()
{
std::cout << "1" << std::endl;
mystring ms;
std::cout << "2" << std::endl;
B b(std::move(ms));
std::cout << "3" << std::endl;
}
You will see:
1
Default
2
Copy
Copy
Move
Delete
Delete
3
Delete
Delete
Your question "In this case, str would be copied twice, or not?" is answered: Yes, it copies twice!
you have always to follow rule of three/five/zero if you also want forwarding/move to work as expected.
This will result into:
class A
{
private:
mystring val1;
public:
A(): val1{}{}
A( const mystring& str ): val1( str ) {}
A(mystring&& str) : val1(std::move(str)){}
// if you also want to assign a string later
A& operator=(const mystring& str ) { std::cout << "copy string to A" << std::endl; val1 = str; return *this; }
A& operator=(mystring&& str ) { std::cout << "move string to A" << std::endl; val1 = (std::move(str)); return *this; }
// and now all the stuff for copy/move of class itself:
A( const A& a): val1{ a.val1} {}
A( A&& a): val1{ std::move(a.val1)} {}
A& operator=( const A& a) { std::cout << "copy from A" << std::endl; val1=a.val1; return *this; }
A& operator=( A&& a) { std::cout << "move from A" << std::endl; val1=std::move(a.val1); return *this; }
};
class B: public A
{
public:
B(): A{}{}
B( const mystring& str ): A( str ) {}
B(mystring&& str) : A(std::move(str)){}
// and now all the stuff for copy/move of class itself:
B( const B& b): A{ b} {}
B( B&& b): A{ std::move(b)} {}
B& operator=( const B& b) { std::cout << "copy from B" << std::endl; A::operator=(b); return *this; }
B& operator=( B&& b) { std::cout << "move from B" << std::endl; A::operator=(std::move(b)); return *this; }
};
And now test also this:
int main()
{
std::cout << "1" << std::endl;
mystring ms;
std::cout << "2" << std::endl;
B b(std::move(ms));
std::cout << "3" << std::endl;
//
std::cout << "4" << std::endl;
B b2;
// and now move assign
std::cout << "5" << std::endl;
b = std::move(b2);
std::cout << "6" << std::endl;
}
Result:
1
Default
2
Move
3
4
Default
5
move from B
move from A
Move assign
6
Delete
Delete
Delete
Now you have a single move for first step and the move assign works also as expected.
See it running here