c++vectoremplacenoncopyable

Emplace a derived movable but noncopyable in a vector gives compilation error


I'm having compiler errors when trying to emplace_back in a vector of non-copyable but movable objects with a subtle inheritance twist, that should to my knowledge not change the problem.

Is this legal C++ and this a Visual Studio 2015 bug, or am I making an obvious mistake ?

#include <vector>

class Base
{
public:
    Base() {}

    Base(Base&) = delete;
    Base& operator= (Base&) = delete;
};

class Test : public Base
{
public:
    Test(int i) : m_i(i) {}

    Test(Test&&) = default;
    Test& operator= (Test&&) = default;

protected:
    int m_i;
};

int main(int argc, char *argv[])
{
    std::vector<Test> vec;
    vec.emplace_back(1);
}

Output :

error C2280: 'Test::Test(Test &)': attempting to reference a deleted function

Without the inheritance, that is with deleted copy constructor in Test and no base class, it compiles correctly.
Somehow, removing the default in the move-constructor make it compile correctly also, but then I have to define the move constructor and I don't want to go there.

Which means this compiles fine :

#include <vector>

class Test
{
public:
    Test(int i) : m_i(i) {}

    Test(Test&) = delete;
    Test& operator= (Test&) = delete;

    Test(Test&&) = default;
    Test& operator= (Test&&) = default;

protected:
    int m_i;
};

int main(int argc, char *argv[])
{
    std::vector<Test> vec;
    vec.emplace_back(1);
}

Puzzling ?


Solution

  • The compiler is correct in all the cases you described.

    When Test is derived from Base, its defaulted move constructor is defined as deleted because it tries to move Base, which cannot be move-constructed. Test is actually not move-constructible in your first example.

    In your second example, with no base class, there's nothing to prevent the defaulted move constructor from being defined, so Test becomes move-constructible.

    When you provide a definition for the move constructor, it's up to you to handle Base. If you just write

    Test(Test&&) { }
    

    this will just default-construct a Base object, so the move constructor will compile, but it probably won't do what you want.


    Base is not move-constructible because it has a user-declared copy constructor, which prevents the implicit declaration of a move constructor - it has no move constructor at all - and its copy constructor is deleted (it couldn't handle rvalues anyway because it takes a non-const reference).

    If you make Base move-constructible, by adding, for example,

    Base(Base&&) = default;
    

    then Test also becomes move-constructible, and your example will compile.


    One last piece of the puzzle: since we have declared a move constructor for Test, why does the error message reference the deleted copy constructor, even in the first case?

    For an explanation of std::vector's logic for choosing which constructor to use to copy / move elements during reallocation, see this answer. Looking at the logic that std::move_if_noexcept uses to choose which kind of reference to return, it will be an rvalue reference in our case (when T is not copy-constructible, the condition is always false). So, we'd still expect the compiler to attempt to call the move constructor.

    However, one more rule comes into play: a defaulted move constructor that is defined as deleted does not participate in overload resolution.

    This is done so that construction from an rvalue can fall back to a copy constructor taking a const lvalue reference, if available. Note that the rule does not apply when the move constructor is explicitly declared as deleted.