Fooling around with move constructors

Flaviu_ 971 Reputation points
2020-09-22T19:18:21.013+00:00

I have a test class:

class CTest
{
public:
    CTest()
    {
        TRACE("c-tor\n");
    }
    virtual ~CTest()
    {
        TRACE("destructor\n");
    }
    CTest(const CTest& rhs) = delete;
    CTest& operator=(const CTest& rhs) = delete;
    CTest(CTest&& rhs)
    {
        TRACE("move c-tor\n");
    }
    CTest& operator=(CTest&& rhs)
    {
        TRACE("move operator\n");
        return *this;
    }

    static void append(CTest* dst, CTest* src)
    {
        TRACE("append\n");
    }
};

and a call:

CTest tst;
CTest tst2;

None of the following call move constructor:

CTest::append(&tst, &tst2);
CTest::append(std::move(&tst), std::move(&tst2));
CTest::append(&tst, new CTest);

Why ?

C++
C++
A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.
3,690 questions
0 comments No comments
{count} votes

Accepted answer
  1. Darran Rowe 916 Reputation points
    2020-09-24T01:51:25.02+00:00

    When you deal with move semantics and pointers, you have to understand what is actually happening.
    What move semantics and r-value references mean is:
    "This is an object that cannot be named which is about to die soon"
    What a pointer means is:
    "This is a named object"
    So a pointer is at odds with the concept of move semantics in the first place.

    So the simplest thing to do is get rid of the pointer from the append parameters. It is neater to use a reference for the first parameter, and you can just not use anything for the second.

    static void append(CTest &dst, CTest src)
    {
        TRACE("append\n");
    }
    

    But you are probably thinking, "wait, this still won't cause it to use the move constructor" and that is true. Calling this with:

    CTest::append(tst, tst2);
    

    will result in a compiler error, but:

    CTest::append(tst, std::move(tst2));
    

    actually goes through the move constructor.

    But there are always problems doing this. Move construction and r-value references are only ever intended to work on objects with no name, like temporary objects, to move the contents to a live object before it goes out of scope. Doing this to an object that has a name is dangerous, after you use std::move (or the related static cast) you must assume that the object that you moved from is dead and treat it with care. Just letting it go out of scope without accessing it is the best option or do a full reinitialisation of the object. It is possible to write the move constructor in such a way that it will leave the object in a good state, but this can be good.

    The better option is to us a function to return the object that you want to move from.

    CTest obtain_test()
    {
        return CTest();
    }
    

    This can then be used like:

    int wmain()
    {
        CTest tst;
    
        CTest::append(tst, obtain_test());
    
        return 0;
    }
    

    It also guarantees that you can never inadvertently access the object you moved from.

    In general, while std::move can be used in this way, one of the big reasons to use it is to add back move capabilities to an r-value reference. For example, suppose you have the classes:

    class test_base
    {
        //
        test_base(test_base &&other)
        {
    
        }
        //
    };
    
    class test_derived : public test_base
    {
        //
        test_derived(test_derived &&other) : test_base(other) //surprise, this calls the copy constructor
        {
    
        }
        //
    };
    

    You may set it up like above and think the derived move constructor will call the base move constructor. But it doesn't, it actually calls the copy constructor. The reason for this is because you have now named the object and is no longer an unnamed object that is about to go out of scope. From this point on, the compiler treats it as an l-value reference. In order to get this to call the base move constructor you have to use std::move:

    test_derived(test_derived &&other) : test_base(std::move(other))
        {
    
        }
    

    This is one of the biggest reasons for std::move. This should also show that you need to set your parameters up in a certain way for the move constructor to actually be called.

    1 person found this answer helpful.

2 additional answers

Sort by: Most helpful
  1. RLWA32 45,236 Reputation points
    2020-09-23T10:54:27.57+00:00

    Play around with this -
    #include <iostream>
    #include <string>
    using namespace std;

    class CTest
    {
    public:
        CTest()
        {
            cout << "default constructor\n";
        }
    
        ~CTest()
        {
            cout << "destructor\n";
        }
    
        CTest(const string strName) : name(strName)
        {
            cout << "non-default constructor\n";
        }
    
        CTest(const CTest& rhs) = delete;
    
        CTest(CTest&& rhs) : name(move(rhs.name))
        {
            cout << "move constructor\n";
        }
    
        CTest& operator=(const CTest& rhs) = delete;
    
        CTest& operator=(CTest&& rhs)
        {
            name = move(rhs.name);
            cout << "move assignment operator\n";
            return *this;
        }
    
        friend ostream& operator<<(ostream& os, const CTest& rhs);
    
    private:
        string name;
    };
    
    ostream& operator<<(ostream& os, const CTest& rhs)
    {
        if (!rhs.name.empty())
            os << "name " << rhs.name << endl;
        else
            os << "name is empty\n";
    
        return os;
    }
    
    void moveprint(CTest&& t)
    {
        CTest tmp(std::move(t));
        cout << tmp;
    }
    
    void moveprint(CTest& tref)
    {
        CTest tmp(std::move(tref));
        cout << tmp;
    }
    
    void moveprint(CTest *ptr)
    {
        CTest tmp(std::move(*ptr));
        cout << tmp;
    }
    
    int main()
    {
        CTest t1("test1"), t2("test2"), t3("test3");
    
        moveprint(std::move(t1));
        cout << t1;
    
        moveprint(t2);
        cout << t2;
    
        moveprint(&t3);
        cout << t3;
    
        return 0;
    }
    
    1 person found this answer helpful.
    0 comments No comments

  2. RLWA32 45,236 Reputation points
    2020-09-22T19:35:30.36+00:00

    std::move does not call a move constructor. It unconditionally casts its argument to an rvalue.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.