Visual Studio 2017 是否需要明确的移动构造函数声明?
Does Visual Studio 2017 need an explicit move constructor declaration?
下面的代码可以使用 Visual Studio 2015 编译成功,但是使用 Visual Studio 2017 编译失败。Visual Studio 2017 报告:
error C2280: “std::pair::pair(const std::pair &)”: attempting to reference a deleted function
代码
#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 explicit 需要移动构造函数声明。这是什么原因?
声明移动构造函数时,隐式声明的复制构造函数被定义为已删除。另一方面,当您不声明移动构造函数时,编译器会在需要时隐式定义复制构造函数。而且这个隐含的定义是错误的。
unique_ptr
在使用标准分配器的容器中不是 CopyInsertable
因为它不是可复制构造的,所以 map_
的复制构造函数格式错误(它本来可以声明为已删除,但这不是标准所要求的)。
正如您的示例代码向我们展示的那样,使用较新版本的 MSVC,此格式错误的定义是使用此示例代码生成的。我认为标准中没有禁止它的东西(即使这真的很令人惊讶)。
所以你确实应该确保 Node 的复制构造函数被声明或隐式定义为已删除。
让我们看一下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
被调用。
问题是,如果您没有显式声明移动构造函数,Node
的类型特征 std::is_copy_constructible_v
是 true
。此声明使复制构造函数被删除。
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
更新
我相信编译错误是合法的。 Vector 的重新分配函数可以使用 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
的复制构造函数。但是,后者在这里可能不会被调用,因为 map 的值类型(std::pair<const int, std::unique_ptr<int>>
在这种情况下)是不可复制的。
最后,如果您自定义 Node
的移动构造函数,其隐式声明的复制构造函数被定义为已删除。 并且,IIRC,删除了隐式声明的复制构造函数不参与重载决议。但是,删除的复制构造函数不被std::move_if_noexcept
考虑,因此它将使用Node.
的throwing move构造函数
Visual Studio 2017:
正如@Evg 指出的那样,Visual Studio 2017 年的矢量源代码最终调用了 _Uninitialized_copy,因为 Node 的隐式声明的移动构造函数被认为是不抛出的(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 说:
The implicitly-declared (or defaulted on its first declaration) move constructor has an exception specification as described in dynamic exception specification (until C++17)exception specification (since C++17)
也许将 is_nothrow_move_constructible<Node>
视为 false 是合理的,因为 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 Studio2015.
可以编译成功
下面的代码可以使用 Visual Studio 2015 编译成功,但是使用 Visual Studio 2017 编译失败。Visual Studio 2017 报告:
error C2280: “std::pair::pair(const std::pair &)”: attempting to reference a deleted function
代码
#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 explicit 需要移动构造函数声明。这是什么原因?
声明移动构造函数时,隐式声明的复制构造函数被定义为已删除。另一方面,当您不声明移动构造函数时,编译器会在需要时隐式定义复制构造函数。而且这个隐含的定义是错误的。
unique_ptr
在使用标准分配器的容器中不是 CopyInsertable
因为它不是可复制构造的,所以 map_
的复制构造函数格式错误(它本来可以声明为已删除,但这不是标准所要求的)。
正如您的示例代码向我们展示的那样,使用较新版本的 MSVC,此格式错误的定义是使用此示例代码生成的。我认为标准中没有禁止它的东西(即使这真的很令人惊讶)。
所以你确实应该确保 Node 的复制构造函数被声明或隐式定义为已删除。
让我们看一下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
被调用。
问题是,如果您没有显式声明移动构造函数,Node
的类型特征 std::is_copy_constructible_v
是 true
。此声明使复制构造函数被删除。
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
更新
我相信编译错误是合法的。 Vector 的重新分配函数可以使用 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
的复制构造函数。但是,后者在这里可能不会被调用,因为 map 的值类型(std::pair<const int, std::unique_ptr<int>>
在这种情况下)是不可复制的。
最后,如果您自定义 Node
的移动构造函数,其隐式声明的复制构造函数被定义为已删除。 并且,IIRC,删除了隐式声明的复制构造函数不参与重载决议。但是,删除的复制构造函数不被std::move_if_noexcept
考虑,因此它将使用Node.
Visual Studio 2017:
正如@Evg 指出的那样,Visual Studio 2017 年的矢量源代码最终调用了 _Uninitialized_copy,因为 Node 的隐式声明的移动构造函数被认为是不抛出的(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 说:
The implicitly-declared (or defaulted on its first declaration) move constructor has an exception specification as described in dynamic exception specification (until C++17)exception specification (since C++17)
也许将 is_nothrow_move_constructible<Node>
视为 false 是合理的,因为 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 Studio2015.