You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用自定义哈希的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 {
      |           ^

错误原因

核心问题是递归依赖导致的不完全类型:

  1. 定义class B时,其成员std::variant包含std::unordered_map<A, B, AHasher>,此时B仅完成前向声明,属于不完全类型。
  2. GCC 9的std::unordered_map实现要求其value_type(即std::pair<const A, B>)必须是完整类型——在实例化unordered_map时,会检查拷贝构造等特性,而这需要B的完整定义。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 18:18:08