Home > Back-end >  Copy and Move Idiom in children classes?
Copy and Move Idiom in children classes?

Time:04-11

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

  • Related