Нужна ли Visual Studio 2017 явное объявление конструктора перемещения?
Приведенный ниже код может быть успешно скомпилирован с использованием Visual Studio 2015, но не удалось с помощью Visual Studio 2017. Отчеты Visual Studio 2017:
ошибка C2280: "std::pair::pair(const std::pair &)": попытка сослаться на удаленную функцию
Код
#include <unordered_map>
#include <memory>
struct Node
{
std::unordered_map<int, std::unique_ptr<int>> map_;
// Uncommenting the following two lines will pass Visual Studio 2017 compilation
//Node(Node&& o) = default;
//Node() = default;
};
int main()
{
std::vector<Node> vec;
Node node;
vec.push_back(std::move(node));
return 0;
}
Похоже, Visual Studio 2017 явно нуждается в объявлении конструктора перемещения. Какова причина?
4 ответа
Давайте посмотрим на std::vector
исходный код (я заменил pointer
а также _Ty
с актуальными типами):
void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, true_type)
{ // move [First, Last) to raw Dest, using allocator
_Uninitialized_move(First, Last, Dest, this->_Getal());
}
void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, false_type)
{ // copy [First, Last) to raw Dest, using allocator
_Uninitialized_copy(First, Last, Dest, this->_Getal());
}
void _Umove_if_noexcept(Node* First, Node* Last, Node* Dest)
{ // move_if_noexcept [First, Last) to raw Dest, using allocator
_Umove_if_noexcept1(First, Last, Dest,
bool_constant<disjunction_v<is_nothrow_move_constructible<Node>, negation<is_copy_constructible<Node>>>>{});
}
Если Node
конструируемо без ходов или не копируемо, _Uninitialized_move
называется, в противном случае, _Uninitialized_copy
называется.
Проблема в том, что черта типа std::is_copy_constructible_v
является true
за Node
если вы не объявляете конструктор перемещения явно. Это объявление удаляет конструктор копирования.
libstdC++ реализует std::vector
аналогичным образом, но есть std::is_nothrow_move_constructible_v<Node>
является true
в отличие от MSVC, где это false
, Итак, семантика перемещения используется, и компилятор не пытается сгенерировать конструктор копирования.
Но если мы заставим is_nothrow_move_constructible_v
становиться false
struct Base {
Base() = default;
Base(const Base&) = default;
Base(Base&&) noexcept(false) { }
};
struct Node : Base {
std::unordered_map<int, std::unique_ptr<int>> map;
};
int main() {
std::vector<Node> vec;
vec.reserve(1);
}
возникает та же ошибка:
/usr/include/c++/7/ext/new_allocator.h:136:4: error: use of deleted function ‘std::pair<_T1, _T2>::pair(const std::pair<_T1, _T2>&) [with _T1 = const int; _T2 = std::unique_ptr<int>]’
{ ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Минимальный пример:
#include <memory>
#include <unordered_map>
#include <vector>
int main() {
std::vector<std::unordered_map<int, std::unique_ptr<int>>> vec;
vec.reserve(1);
}
Живая демоверсия о GodBolt: https://godbolt.org/z/VApPkH.
Другой пример:
std::unordered_map<int, std::unique_ptr<int>> m;
auto m2 = std::move(m); // ok
auto m3 = std::move_if_noexcept(m); // error C2280
ОБНОВИТЬ
Я считаю, что ошибка компиляции законна. Функция перераспределения вектора может передавать (содержимое) элементов с помощью std::move_if_noexcept
, следовательно, предпочитая копировать конструкторы бросая конструкторы перемещения.
В libstdC++ (GCC) / libC++ (clang), переместите конструктор std::unordered_map
есть (казалось бы) noexcept
, Следовательно, переместите конструктор Node
является noexcept
также, и его конструктор копирования вообще не вовлечен.
С другой стороны, реализация из MSVC 2017, похоже, не указывает конструктор перемещения std::unordered_map
как noexcept
, Поэтому переместите конструктор Node
не является noexcept
а также функция перераспределения вектора через std::move_if_noexcept
пытается вызвать конструктор копирования Node
,
Копировать конструктор Node
неявно определяется так, что вызывает конструктор копирования std::unordered_map
, Тем не менее, последний не может быть вызван здесь, так как тип значения карты (std::pair<const int, std::unique_ptr<int>>
в этом случае) не копируется.
Наконец, если вы определяете пользовательский конструктор перемещения Node
, его неявно объявленный конструктор копирования определяется как удаленный. И, IIRC, удаленный неявно объявленный конструктор копирования не участвует в разрешении перегрузки. Но конструктор удаленных копий не учитывается std::move_if_noexcept
поэтому он будет использовать конструктор броска Node.
Когда вы объявляете конструктор перемещения, неявно объявленный конструктор копирования определяется как удаленный. С другой стороны, когда вы не объявляете конструктор перемещения, компилятор неявно определяет конструктор копирования, когда это необходимо. И это неявное определение плохо сформировано.
unique_ptr
не является CopyInsertable
в контейнере, который использует стандартный распределитель, потому что он не копируемый, поэтому конструктор копирования map_
неправильно сформирован (он мог быть объявлен как удаленный, но это не требуется стандартом).
Как показывает ваш пример кода, в более новой версии MSVC это плохо сформированное определение создается с помощью этого примера кода. Я не думаю, что в стандарте есть что-то, что запрещает это (даже если это действительно удивительно).
Таким образом, вы действительно должны убедиться, что конструктор копирования Node объявлен или неявно определен как удаленный.
Visual Studio 2017:
Как указывало @Evg, векторный исходный код Visual Studio 2017, наконец, вызывает _Uninitialized_copy, поскольку неявно объявленный конструктор перемещения Node считается not-nothrow (is_nothrow_move_constructible<Node>
ложно) и is_copy_constructible<Node>
верно в Visual Studio 2017.
1) О is_nothrow_move_constructible<Node>
:
https://en.cppreference.com/w/cpp/language/move_constructor говорит:
Неявно объявленный (или по умолчанию в своем первом объявлении) конструктор перемещения имеет спецификацию исключений, как описано в спецификации исключений динамических исключений (до C++17) (начиная с C++17)
Может быть, это разумно рассмотреть is_nothrow_move_constructible<Node>
как ложная причина Node
член данных std::unordered_map
Конструктор ходов не помечен как noexcept.
2) О is_copy_constructible<Node>
:
Как говорит @Oliv, на первый взгляд нелогично вычислять is_copy_constructible<Node>
как правда, особенно учитывая тот факт, что Node
не является copy_constructible было обнаружено и сообщается как ошибка компиляции компилятором Visual Studio 2017. Node
это не copy_constructible, потому что std::unique_ptr
не является copy_constructible.
Visual Studio 2015:
Вектор Visual Studio 2015 имеет другую реализацию. vec.push_back
-> _Reserve
-> _Reallocate
-> _Umove
-> _Uninitialized_move_al_unchecked
-> _Uninitialized_move_al_unchecked1
-> std::move(node)
, is_nothrow_move_constructible<Node>
а также is_copy_constructible<Node>
не участвуют. Это просто вызов std::move(node)
вместо конструктора копирования. Таким образом, пример кода может быть успешно скомпилирован с помощью Visual Studio 2015.