std :: vector :: push_back() не компилируется в MSVC для объекта с удаленным конструктором перемещения

У меня есть класс с удаленным конструктором перемещения, и когда я пытаюсь вызвать std :: vector :: push_back() в MSVC (v.15.8.7 Visual C++ 2017), я получаю сообщение о том, что я пытаюсь получить доступ к удаленный конструктор перемещения. Однако, если я определяю конструктор перемещения, код компилируется, но конструктор перемещения никогда не вызывается. Обе версии компилируются и запускаются, как и ожидалось, в gcc (v. 5.4).

Вот упрощенный пример:

#include <iostream>
#include <vector>

struct A
{
public:
    A() { std::cout << "ctor-dflt" << std::endl; }
    A(const A&) { std::cout << "ctor-copy" << std::endl; }
    A& operator=(const A&) { std::cout << "asgn-copy" << std::endl; return *this; }
    A(A&&) = delete;
    A& operator=(A&& other) = delete;
    ~A() { std::cout << "dtor" << std::endl; }
};


int main()
{
    std::vector<A> v{};
    A a;
    v.push_back(a);
}

который при компиляции в Visual Studio выдает следующую ошибку:

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

Однако, если я определяю конструктор перемещения вместо его удаления

 A(A&&) { std::cout << "ctor-move" << std::endl; }

все компилируется и запускается со следующим выводом:

ctor-dflt
ctor-copy
dtor
dtor

как и ожидалось. Нет вызовов конструктору перемещения. (Живой код: https://rextester.com/XWWA51341)

Более того, обе версии прекрасно работают на gcc. (Живой код: https://rextester.com/FMQERO10656)

Итак, мой вопрос: почему вызов std :: vector :: push_back() для неподвижного объекта не компилируется в MSVC, хотя конструктор перемещения, очевидно, никогда не вызывается?

Ответ 1

std::vector<T>::push_back() требует, чтобы T удовлетворял концепции MoveInsertable (которая фактически включает в себя распределитель Alloc). Это потому, что push_back для вектора может потребоваться увеличить вектор, переместив (или скопировав) все элементы, которые уже есть в нем.

Если вы объявите c'or для T как удаленный, то, по крайней мере для распределителя по умолчанию (std::allocator<T>), T больше не будет MoveInsertable. Обратите внимание, что это отличается от случая, когда конструктор перемещения не объявлен, например, потому что неявно генерируется только копия c'tor, или потому что была объявлена только копия c'tor, и в этом случае тип по-прежнему MoveInsertable, но копия c'tor на самом деле называется (это немного нелогично TBH).

Причина, по которой перемещение c'tor на самом деле никогда не вызывается, заключается в том, что вы вставляете только один элемент, и поэтому перемещение существующих элементов не требуется во время выполнения. Важно, что ваш аргумент для push_back сам по себе является lvalue и поэтому копируется и ни в коем случае не перемещается.

ОБНОВЛЕНИЕ: я должен был рассмотреть это более внимательно (благодаря комментариям в комментариях). Версии MSVC, которые отклоняют код, на самом деле правильны (очевидно, и 2015, и до 2018 года делают это, но 2017 принимает код). Поскольку вы вызываете push_back(const T&), T должен быть CopyInsertable. Однако CopyInsertable определяется как строгое подмножество MoveInsertable. Поскольку ваш тип не является MoveInsertable, он также не является CopyInsertable по определению (обратите внимание, что, как объяснено выше, тип может удовлетворять обеим концепциям, даже если он является только копируемым, если только c Move не был явно удален),

Это, конечно, поднимает еще несколько вопросов: (A) Почему GCC, Clang и некоторые версии MSVC все-таки принимают код, и (B) нарушают ли они стандарт?

Что касается (A), нет другого способа узнать, кроме как поговорить с разработчиками стандартной библиотеки или посмотреть на исходный код… я бы хотел догадаться, что просто не нужно реализовывать эту проверку, если все, что вас волнует, это создание легальных программ Работа. Перемещение существующих элементов во время push_back (или reserve и т.д.) Может происходить любым из трех способов в соответствии со стандартом:

  • Если std::is_nothrow_move_constructible_v<T>, то элементы не перемещаются (и операция строго безопасна для исключений).
  • В противном случае, если T - CopyInsertable, то элементы копируются (и операция строго безопасна для исключений).
  • В противном случае элементы перемещаются (и эффекты исключений, возникающих в ходе перемещения, не определены).

Так как ваш тип не только перемещается, но имеет копию или нет, можно выбрать второй вариант. Это снисходительно, так как проверка MoveInsertable не выполняется. Это может быть недосмотр реализации, или я мог быть намеренно проигнорирован. (Если тип не MoveInsertable, то весь вызов некорректен, поэтому эта пропущенная проверка не влияет на правильно сформированные программы.)

Что касается (B), IMO реализации, которые принимают код, нарушают стандарт, потому что они не производят диагностику. Это связано с тем, что реализация должна выдавать диагностику для плохо сформированных программ (это включает в себя программы, использующие языковые расширения, предоставляемые реализацией), если в стандарте не указано иное, в данном случае это не так.

Ответ 2

Это неопределенное поведение, поэтому и gcc, и MSVC верны.

Недавно я написал в Твиттере о похожем случае, используя std :: vector :: emplace_back с типом, который имеет удаленный конструктор перемещения и, как и этот, это неопределенное поведение. Таким образом, все компиляторы здесь верны, неопределенное поведение не требует диагностики, хотя реализации могут сделать это бесплатно.

Мы можем увидеть причину, начав с [container.requirements.general] Таблицы 88, которая говорит нам, что push_back требует, чтобы T был CopyInsertable:

Требуется: T должен быть CopyInsertable в x

и мы видим, что CopyInsertable требует MoveInsertable [container.requirements # general] p15:

T является CopyInsertable в X означает, что в дополнение к T, будучи MoveInsertable в X...

В этом случае A не MoveInsertable.

Мы можем видеть, что это неопределенное поведение, выполняя поиск в [res.on.required] p1:

Нарушение предварительных условий, указанных в функции Требуется: абзац приводит к неопределенному поведению, если только функция Throws: абзац не определяет выбрасывание исключения при нарушении предусловия.

[res.on.required] подпадает под общебиблиотечные требования.

В этом случае у нас нет параграфа throws, поэтому мы имеем неопределенное поведение, которое не требует диагностики, как мы видим из его определения:

поведение, для которого этот международный стандарт не предъявляет никаких требований....

Обратите внимание, это очень отличается от того, что плохо сформирован, что требует диагностики, я объясню все детали в моем ответе здесь.