Нужна ли Visual Studio 2017 явное объявление конструктора перемещения? - PullRequest
0 голосов
/ 06 ноября 2018

Приведенный ниже код может быть успешно скомпилирован с использованием 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 ]

0 голосов
/ 07 ноября 2018

Visual Studio 2017:

Как указано @Evg, векторный исходный код Visual Studio 2017, наконец, вызывает _Uninitialized_copy, поскольку неявно объявленный конструктор перемещения Node считается not-nothrow (is_nothrow_move_constructible<Node> - false), а is_copy_constructible<Node> - true в Visual Studio 2017 .

1) О is_nothrow_move_constructible<Node>:

https://en.cppreference.com/w/cpp/language/move_constructor говорит:

Неявно объявленный (или по умолчанию установленный в первом объявлении) конструктор перемещения имеет спецификацию исключений, как описано в спецификация динамических исключений (до C ++ 17) спецификация исключений (поскольку С ++ 17) * +1021 *

Возможно, разумно считать 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 не копируется, потому что std::unique_ptr не копируется.

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.

0 голосов
/ 06 ноября 2018

Давайте посмотрим на исходный код 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)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
0 голосов
/ 06 ноября 2018

Минимальный пример:

#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

UPDATE

Я считаю, что ошибка компиляции допустима. Функция перераспределения вектора может передавать (содержимое) элементов, используя 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.

0 голосов
/ 06 ноября 2018

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

unique_ptr не является CopyInsertable в контейнере, который использует стандартный распределитель, потому что он не может быть создан для копирования, поэтому конструктор копирования map_ имеет неправильную форму (он мог быть объявлен как удаленный, но это не так требуется стандартом).

Как показывает ваш пример кода, в более новой версии MSVC это плохо сформированное определение создается с помощью этого примера кода. Я не думаю, что в стандарте есть что-то, что запрещает это (даже если это действительно удивительно).

Таким образом, вы действительно должны убедиться, что конструктор копирования Node объявлен или неявно определен как удаленный.

...