使用自定义哈希的std::unordered_map触发拷贝构造相关编译错误
问题分析:std::variant包含递归unordered_map时的编译错误
问题复现
代码示例
#include <initializer_list> #include <string> #include <unordered_map> #include <variant> namespace myNamespace { class A; struct AHasher; class B; class A { public: A(const std::string& str); friend bool operator==(const A& lhs, const A& rhs); public: std::string value; }; struct AHasher { std::size_t operator()(const A& str) const; }; class B { public: B(); B(const std::string& str); B(const A& str); B(const std::initializer_list<std::pair<const A, B>> list); private: std::variant<A, std::unordered_map<A, B, AHasher>> value; }; } // namespace myNamespace namespace myNamespace { A::A(const std::string& str) : value(str) {} bool operator==(const A& lhs, const A& rhs) { return lhs.value == rhs.value; } std::size_t AHasher::operator()(const A& str) const { std::hash<std::string> hasher; return hasher(str.value); } B::B() : value(std::unordered_map<A, B, AHasher>{}) {} B::B(const std::string& str) : value(A(str)) {} B::B(const A& str) : value(str) {} B::B(const std::initializer_list<std::pair<const A, B>> list) : value(list) {} } // namespace myNamespace int main() { return 0; }
编译命令
g++ -std=c++17 -Wall -Wextra -pedantic -O0 main.cpp
错误信息
In file included from /usr/include/c++/9/bits/stl_algobase.h:64, from /usr/include/c++/9/bits/char_traits.h:39, from /usr/include/c++/9/string:40, from tmp.cpp:53: /usr/include/c++/9/bits/stl_pair.h: In instantiation of ‘struct std::pair<const myNamespace::A, myNamespace::B>’: /usr/include/c++/9/ext/aligned_buffer.h:91:28: required from ‘struct __gnu_cxx::__aligned_buffer<std::pair<const myNamespace::A, myNamespace::B> >’ /usr/include/c++/9/bits/hashtable_policy.h:233:43: required from ‘struct std::__detail::_Hash_node_value_base<std::pair<const myNamespace::A, myNamespace::B> >’ /usr/include/c++/9/bits/hashtable_policy.h:264:12: required from ‘struct std::__detail::_Hash_node<std::pair<const myNamespace::A, myNamespace::B>, true>’ /usr/include/c++/9/bits/hashtable_policy.h:2027:13: required from ‘struct std::__detail::_Hashtable_alloc<std::allocator<std::__detail::_Hash_node<std::pair<const myNamespace::A, myNamespace::B>, true> > >’ /usr/include/c++/9/bits/hashtable.h:173:11: required from ‘class std::_Hashtable<myNamespace::A, std::pair<const myNamespace::A, myNamespace::B>, std::allocator<std::pair<const myNamespace::A, myNamespace::B> >, std::__detail::_Select1st, std::equal_to<myNamespace::A>, myNamespace::AHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >’ /usr/include/c++/9/bits/unordered_map.h:105:18: [ skipping 2 instantiation contexts, use -ftemplate-backtrace-limit=0 to disable ] /usr/include/c++/9/type_traits:901:12: required from ‘struct std::__is_copy_constructible_impl<std::unordered_map<myNamespace::A, myNamespace::B, myNamespace::AHasher>, true>’ /usr/include/c++/9/type_traits:907:12: required from ‘struct std::is_copy_constructible<std::unordered_map<myNamespace::A, myNamespace::B, myNamespace::AHasher> >’ /usr/include/c++/9/type_traits:2918:25: required from ‘constexpr const bool std::is_copy_constructible_v<std::unordered_map<myNamespace::A, myNamespace::B, myNamespace::AHasher> >’ /usr/include/c++/9/variant:275:5: required from ‘constexpr const bool std::__detail::__variant::_Traits<myNamespace::A, std::unordered_map<myNamespace::A, myNamespace::B, myNamespace::AHasher, std::equal_to<myNamespace::A>, std::allocator<std::pair<const myNamespace::A, myNamespace::B> > > >::_S_copy_ctor’ /usr/include/c++/9/variant:1228:11: required from ‘class std::variant<myNamespace::A, std::unordered_map<myNamespace::A, myNamespace::B, myNamespace::AHasher, std::equal_to<myNamespace::A>, std::allocator<std::pair<const myNamespace::A, myNamespace::B> > > >’ tmp.cpp:81:64: required from here /usr/include/c++/9/bits/stl_pair.h:215:11: error: ‘std::pair<_T1, _T2>::second’ has incomplete type 215 | _T2 second; /// @c second is a copy of the second object | ^~~~~~ tmp.cpp:74:11: note: forward declaration of ‘class myNamespace::B’ 74 | class B { | ^
错误原因
核心问题是递归依赖导致的不完全类型:
- 定义
class B时,其成员std::variant包含std::unordered_map<A, B, AHasher>,此时B仅完成前向声明,属于不完全类型。 - GCC 9的
std::unordered_map实现要求其value_type(即std::pair<const A, B>)必须是完整类型——在实例化unordered_map时,会检查拷贝构造等特性,而这需要B的完整定义。 std::variant在实例化阶段会验证所有备选类型的可拷贝构造性,触发了对unordered_map的类型检查,进而暴露B是不完全类型的问题。
而之前用std::map正常,是因为std::map基于红黑树实现,GCC对其的兼容更好,C++17及以后标准允许不完全类型作为map的value(只要后续完成完整定义),但GCC 9的std::unordered_map不支持这一点。
解决方案
方案1:用智能指针打破递归依赖
将unordered_map的value类型改为std::unique_ptr<B>(或std::shared_ptr<B>),此时std::pair<const A, std::unique_ptr<B>>中的指针类型不需要B是完整类型,仅前向声明即可。修改后的关键代码如下:
namespace myNamespace { class B { public: B(); B(const std::string& str); B(const A& str); // 调整初始化列表参数类型 B(const std::initializer_list<std::pair<const A, std::unique_ptr<B>>> list); private: // 修改variant的备选类型 std::variant<A, std::unordered_map<A, std::unique_ptr<B>, AHasher>> value; }; // 构造函数实现调整 B::B(const std::initializer_list<std::pair<const A, std::unique_ptr<B>>> list) : value(std::unordered_map<A, std::unique_ptr<B>, AHasher>(list)) {} }
使用时需要通过std::make_unique<B>创建对象,比如:
B obj({ {A("key1"), std::make_unique<B>("value1")}, {A("key2"), std::make_unique<B>(A("value2"))} });
方案2:升级GCC版本
GCC 11及以后的std::unordered_map实现支持将不完全类型作为value(符合C++20标准的提前实现),升级到GCC 11+后,原代码无需修改即可正常编译。
方案3:延迟variant实例化(复杂不推荐)
通过模板或间接包装的方式,让std::variant的实例化延迟到B完整定义之后,比如用std::unique_ptr<std::variant<...>>作为成员,或者使用模板基类。但这种方式会增加代码复杂度,仅在无法使用前两种方案时考虑。
内容的提问来源于stack exchange,提问作者TheMemeMachine
相关产品推荐
相关产品推荐

