web-dev-qa-db-fra.com

Visual Studio 2017 a-t-il besoin d'une déclaration du constructeur de déplacement explicite?

Le code ci-dessous peut être compilé avec succès à l'aide de Visual Studio 2015, mais il a échoué avec Visual Studio 2017. Visual Studio 2017 indique:

erreur C2280: “std :: pair :: pair (const std :: pair &)”: tentative de référencer une fonction supprimée

Code

#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;
}

Il semble que Visual Studio 2017 explicit nécessite une déclaration du constructeur du déplacement. Quelle est la raison?

14
finn

Regardons le code source std::vector (j'ai remplacé pointer et _Ty par des types réels):

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>>>>{});
}

Si Node est no-throw move-constructible ou not-copy-constructible , _Uninitialized_move est appelé, sinon, _Uninitialized_copy est appelé.

Le problème est que le trait trait std::is_copy_constructible_v est true pour Node si vous ne déclarez pas explicitement un constructeur de déplacement. Cette déclaration rend le constructeur de copie supprimé.

libstdc ++ implémente std::vector de la même manière, mais std::is_nothrow_move_constructible_v<Node> est true contrairement à MSVC, où il s'agit de false. Donc, la sémantique de déplacement est utilisée et le compilateur n'essaie pas de générer le constructeur de copie.

Mais si on force is_nothrow_move_constructible_v à devenir 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);
}

la même erreur se produit:

/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)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
Evg

Exemple minimal:

#include <memory>
#include <unordered_map>
#include <vector>

int main() {
  std::vector<std::unordered_map<int, std::unique_ptr<int>>> vec;
  vec.reserve(1);
}

Démo en direct sur GodBolt: https://godbolt.org/z/VApPkH .


Un autre exemple:

std::unordered_map<int, std::unique_ptr<int>> m;
auto m2 = std::move(m);              // ok
auto m3 = std::move_if_noexcept(m);  // error C2280

METTRE &AGRAVE; JOUR

Je crois que l'erreur de compilation est légale. La fonction de réallocation de Vector peut transférer (le contenu de) des éléments à l'aide de std::move_if_noexcept, préférant par conséquent les constructeurs de copie à ceux de déplacement.

Dans libstdc ++ (GCC)/libc ++ (clang), le constructeur de déplacement de std::unordered_map est (apparemment) noexcept. Par conséquent, le constructeur de mouvement de Node est également noexcept et son constructeur de copie n'est pas du tout impliqué. 

D'autre part, la mise en œuvre à partir de MSVC 2017 ne spécifie apparemment pas que le constructeur de mouvement de std::unordered_map est noexcept. Par conséquent, le constructeur de déplacement de Node n'est pas non plus noexcept, et la fonction de réallocation de vector via std::move_if_noexcept tente d'appeler le constructeur de copie de Node.

Le constructeur de copie de Node est défini implicitement de manière à invoquer le constructeur de copie de std::unordered_map. Toutefois, cette dernière ne peut pas être invoquée ici, car le type de valeur de la carte (std::pair<const int, std::unique_ptr<int>> dans ce cas) n'est pas copiable.

Enfin, si vous définissez le constructeur de déplacement de Node, son constructeur de copie implicitement déclaré est défini comme étant supprimé. Et, IIRC, le constructeur de copie supposé implicitement supprimé ne participe pas à la résolution de surcharge. Cependant, le constructeur de copie supprimé n'est pas considéré par std::move_if_noexcept, il utilisera donc le constructeur de mouvements de projection de Node.

9
Daniel Langr

Lorsque vous déclarez un constructeur de déplacement, le constructeur de copie déclaré implicitement est défini comme étant supprimé. D'autre part, lorsque vous ne déclarez pas de constructeur de déplacement, le compilateur définit implicitement le constructeur de copie lorsqu'il en a besoin. Et cette définition implicite est mal formée.

unique_ptr n'est pas CopyInsertable dans un conteneur qui utilise un allocateur standard car il n'est pas constructible en copie, le constructeur de copie de map_ est donc mal formé (il aurait pu être déclaré comme supprimé, mais le standard ne l'exige pas. 

Comme votre exemple de code nous le montre, dans les versions plus récentes de MSVC, cette définition mal formée est générée avec cet exemple de code. Je ne pense pas qu'il y ait quelque chose dans la norme qui l'interdit (même si c'est vraiment surprenant).

Vous devez donc vous assurer que le constructeur de la copie de Node est déclaré ou implicitement défini comme étant supprimé.

6
Oliv