简体   繁体   中英

move and copy semantics with the std::vector

How does one control which constructor/assignment operator is being used to insert elements into the std::vector class? I tried to do it by delete ing the constructor/assignment I wanted to avoid using as follows

#include<iostream>
#include<vector>
using namespace std;

class copyer{
    double d;
public:
    //ban moving
    copyer(copyer&& c) = delete;
    copyer& operator=(copyer&& c) = delete;
    //copy construction
    copyer(const copyer& c){
        cout << "Copy constructor!" << endl;
        d = c.d;
    }
    copyer& copy(const copyer& c){
        cout << "Copy constructor!" << endl;
        d = c.d;
        return *this;
    }
    //Constructor
    copyer(double s) : d(s) { }
    double fn(){return d;}
};

class mover{
    double d;
public:
    //ban copying
    mover(const mover& c) = delete;
    mover& operator=(const mover& c) = delete;
    //move construction
    mover(mover&& c){
        cout << "Move constructor!" << endl;
        d = c.d;
    }
    mover& copy(mover&& c){
        cout << "Move constructor!" << endl;
        d = c.d;
        return *this;
    }
    //Constructor
    mover(double s) : d(s) { }
    double fn(){return d;}
};

template<class P> class ConstrTests{
    double d;
    size_t N;
    std::vector<P> object;
public:
    ConstrTests(double s, size_t n) : d(s) , N(n) {
        object.reserve(N);
        for(int i = 0; i<N; i++){
            object.push_back(P((double) i*d));
        }
    }
    void test(){
        int i = 0;
        while(i<N){
            cout << "Testing " <<i+1 << "th object: " << object.at(i).fn();
            i++;
        }
    }
};

When I compile and run

size_t N = 10;
double d = 4.0;
ConstrTests<mover> Test1 = ConstrTests<mover>(d,N);
Test1.test();

I have no problems, but if instead I try

size_t N = 10;
double d = 4.0;
ConstrTests<copyer> Test1 = ConstrTests<copyer>(d,N);
Test1.test();

I get an error at compilation stating I'm trying to use a deleted move constructor.

If you remove these lines from copyer

//ban moving
copyer(copyer&& c) = delete;
copyer& operator=(copyer&& c) = delete;

then the code compiles fine and works as expected, that is, std::vector<copyer> uses the copy-constructor and std::vector<mover> uses the move-constructor.

You had declared copyer 's move constructor and defined it as deleted. It means that copyer::copyer(copyer&& c) participates in overload resolution but if selected the code is ill formed. The call object.push_back(P((double) i*d)); triggers this call.

Why removing the lines above fixed the issue?

In old C++98 if we don't declare a copy-constructor the compiler declares and implement one for us. In C++11 this rules has changed a little bit. The compiler will not implicitly define a copy-constructor if a move constructor is user declared. (There's more on this subject but for this discussion this is enough.) Analogously, if we don't declare a move-constructor the compiler will implicitly define one for us unless we declare (for instance) a copy-constructor.

Now, after having removed the lines above, copyer will have a user declared copy-constructor buy not a move-constructor. Then, neither you nor the compiler would have declared a move-constructor. In that case, object.push_back(P((double) i*d)); will trigger a call to the copy-constructor. Notice that this is exaclty what would have happened if we compiled with a C++98 compliant compiler. In this case backward compatibility holds and old code doesn't break.

The version which is called, is determined by overload resolution. If you call push_back with a prvalue or a xvalue it'll use the move constructor, assuming your move constructor is noexcept, if it is not, it'll still use the copy constructor, if the copy constructor is deleted, you're forcing the vector to use your throwing move constructor, in which case the vector can no longer provide the strong exception guarantee. (Ie. If an exception happens from your move constructor, you've broken the content of the vector).

The general idea is, that whenever you do a push_back, reallocation may be needed, if this is the case, all elements will have to be transfered to the new memory block, using either a move or a copy constructor. Using the move constructor is generally nice, if we're sure it won't throw anything, because in the case it does, we've may already have moved some objects from the original vector, which is now broken. And moving them back is not an alternative as that may throw as well. This is why you loose the strong exception guarantee.

So in general, please do declare your move constructor noexcept, if you'd like it to be called over the copy constructor. (In the above example, if the copy constructor throws, you can just deallocate the new memory, and rethrow the exception, and the vector is still as before the push_back call).

So in general, you control which is called, based upon which constructors are available, and upon the type of the argument (lvalue vs prvalue/xvalue). Assuming your constructors are in place, you'll likely control it, by calling push_back, using std::move on it's argument, this effectively converts the argument to an xvalue, if possible.

Note; whatever I said about push_back, generally applies to insert as well. Note2: the guarantee I'm talking about is, that whenever push_back is called, we're guaranteed that if an exception happens, the call will not have any effect, this is known as the strong exception guarantee, and it cannot be provided if all you got is a throwing move constructor.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM