web-dev-qa-db-ja.com

Visual Studio 2017には明示的な移動コンストラクター宣言が必要ですか?

以下のコードは、Visual Studio 2015を使用して正常にコンパイルできますが、Visual Studio2017を使用すると失敗しました。VisualStudio2017のレポート:

エラー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 Studio2017明示的にmoveコンストラクター宣言が必要なようです。理由は何ですか?

14
finn

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

Nodeno-throw move-constructibleまたはnot copy-constructibleの場合、_Uninitialized_moveが呼び出されます。それ以外の場合、_Uninitialized_copyと呼ばれます。

問題は、移動コンストラクターを明示的に宣言しない場合、型特性std::is_copy_constructible_vtrueに対してNodeになることです。この宣言により、copy-constructorが削除されます。

libstdc ++は同様の方法でstd::vectorを実装しますが、trueであるMSVCとは対照的に、std::is_nothrow_move_constructible_v<Node>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)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
Evg

最小限の例:

#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のmoveコンストラクターは(一見)noexceptです。したがって、Nodeの移動コンストラクターもnoexceptであり、そのコピーコンストラクターはまったく関与していません。

一方、MSVC 2017からの実装では、std::unordered_mapのmoveコンストラクターをnoexceptとして指定していないようです。したがって、Nodeのmoveコンストラクターもnoexceptではなく、std::move_if_noexceptを介したベクターの再割り当て関数はNodeのコピーコンストラクターを呼び出そうとします。

Nodeのコピーコンストラクターは、std::unordered_mapのコピーコンストラクターを呼び出すように暗黙的に定義されています。ただし、マップの値型(この場合はstd::pair<const int, std::unique_ptr<int>>)はコピーできないため、後者はここでは呼び出されない場合があります。

最後に、Nodeのmoveコンストラクターをユーザー定義すると、暗黙的に宣言されたコピーコンストラクターは削除済みとして定義されます。 そして、IIRC、削除された暗黙的に宣言されたコピーコンストラクターは過負荷解決に参加しません。ただし、削除されたコピーコンストラクターはstd::move_if_noexceptによって考慮されないため、Node.のスロー移動コンストラクターを使用します。

9
Daniel Langr

移動コンストラクターを宣言すると、暗黙的に宣言されたコピーコンストラクターは削除済みとして定義されます。一方、moveコンストラクターを宣言しない場合、コンパイラーは必要なときにcopyコンストラクターを暗黙的に定義します。そして、この暗黙の定義は形式が正しくありません。

unique_ptrは、標準アロケーターを使用するコンテナーではCopyInsertableではありません。これは、コピーコンストラクターが作成できないため、map_のコピーコンストラクターの形式が正しくないためです(削除済みとして宣言されている可能性があります。ただし、これは標準では必須ではありません)。

サンプルコードが示すように、新しいバージョンのMSVCでは、この不正な定義がこのサンプルコードで生成されます。私はそれを禁止するものが規格にあるとは思いません(これが本当に驚くべきことであっても)。

したがって、Nodeのコピーコンストラクタが宣言されているか、暗黙的に削除済みとして定義されていることを確認する必要があります。

6
Oliv

Visual Studio 2017:

@Evgが示すように、Visual Studio 2017のベクターソースコードは最終的に_Uninitialized_copyを呼び出します。これは、暗黙的に宣言されたNodeはnot-nothrow(is_nothrow_move_constructible<Node>はfalse)と見なされ、is_copy_constructible<Node>はVisualではtrueであるため)スタジオ2017。

1)is_nothrow_move_constructible<Node>について:

https://en.cppreference.com/w/cpp/language/move_constructor 言います:

暗黙的に宣言された(または最初の宣言でデフォルトにされた)moveコンストラクターには、 動的例外仕様 (C++ 17まで) 例外仕様 (C以降)で説明されている例外仕様があります。 ++ 17)

Nodeのデータメンバーis_nothrow_move_constructible<Node>のmoveコンストラクターがnoexceptとしてマークされていないため、std::unordered_mapをfalseと見なすのが妥当かもしれません。

2)is_copy_constructible<Node>について:

@Olivが言うように、特にNodeがcopy_constructibleではないという事実を考慮して、is_copy_constructible<Node>をtrueとして計算することは論理的ではないようです。これは、Visual Studio2017コンパイラによってコンパイルエラーとして検出および報告されています。 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 Studio2015を使用して正常にコンパイルできます。

0
finn