以下のコードは、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コンストラクター宣言が必要なようです。理由は何ですか?
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
がno-throw move-constructibleまたはnot copy-constructibleの場合、_Uninitialized_move
が呼び出されます。それ以外の場合、_Uninitialized_copy
と呼ばれます。
問題は、移動コンストラクターを明示的に宣言しない場合、型特性std::is_copy_constructible_v
がtrue
に対して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)...); }
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
最小限の例:
#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.
のスロー移動コンストラクターを使用します。
移動コンストラクターを宣言すると、暗黙的に宣言されたコピーコンストラクターは削除済みとして定義されます。一方、moveコンストラクターを宣言しない場合、コンパイラーは必要なときにcopyコンストラクターを暗黙的に定義します。そして、この暗黙の定義は形式が正しくありません。
unique_ptr
は、標準アロケーターを使用するコンテナーではCopyInsertable
ではありません。これは、コピーコンストラクターが作成できないため、map_
のコピーコンストラクターの形式が正しくないためです(削除済みとして宣言されている可能性があります。ただし、これは標準では必須ではありません)。
サンプルコードが示すように、新しいバージョンのMSVCでは、この不正な定義がこのサンプルコードで生成されます。私はそれを禁止するものが規格にあるとは思いません(これが本当に驚くべきことであっても)。
したがって、Nodeのコピーコンストラクタが宣言されているか、暗黙的に削除済みとして定義されていることを確認する必要があります。
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を使用して正常にコンパイルできます。