为什么这包含订单会导致 unordered_map 上的 link 错误?

Why this include order causes link error on unordered_map?

我无法解释包含顺序的问题。我将向您展示一个包含四个文件的最小示例:

// A.h
#pragma once
#include <functional>

struct A {};

namespace std {
    template<>
    class hash<A> {
    public:
        size_t operator()(const A&) const {
            return 0;
        };
    };
}

// B.h
#pragma once
#include <unordered_map>

struct A;

struct B {
    const std::unordered_map<A, int>& GetMap() const;
};

// B.cpp
#include "B.h"
#include "A.h"

const std::unordered_map<A, int>& B::GetMap() const {
    static std::unordered_map<A, int> m;
    return m;
}

// main.cpp
#include "A.h" // To be included AFTER B.h
#include "B.h"

int main() {
    B b{};
    const auto& m = b.GetMap();
}

在这个例子中我得到以下错误:

error LNK2019: unresolved external symbol "public: class std::unordered_map<struct A,int,class std::hash<struct A>,struct std::equal_to<struct A>,class std::allocator<struct std::pair<struct A const ,int> > > const & __cdecl B::GetMap(void)const " (?GetMap@B@@QEBAAEBV?$unordered_map@UA@@HV?$hash@UA@@@std@@U?$equal_to@UA@@@3@V?$allocator@U?$pair@$$CBUA@@H@std@@@3@@std@@XZ) referenced in function main
1>Z:\Shared\sources\Playground\x64\Debug\Playground.exe : fatal error LNK1120: 1 unresolved externals

但是如果在 main.cpp 中我在 B.h 之后包含 A.h,程序编译成功。 有人可以解释为什么吗?

找了很久才在实际代码中找到问题,有什么方法可以让我很容易理解这个错误与include order有关吗?

编辑: 我做了一些其他测试来调查这个问题。

如果我将 std::unordered_map<A, int> 更改为 std::unordered_set<A> 而不是 std::map<A, int>std::set<A>,也会出现此错误,因此我认为散列存在一些问题.

如建议的那样,在 B.h 中包含 A.h 而不是向前声明 A 使得构建成功而无需修改 main.cpp 中的包含顺序。

所以我认为问题变成了:为什么前向声明 A 并因此具有无序映射的键的不完整类型会导致错误?

我在 Visual Studio 2022 中测试了相同的代码并得到了相同的错误。经过我的探索,我发现了问题。

首先,我将 A.h 和 B.h 的内容复制到 main.cpp 中,并删除了 #include 指令。编译后还是报同样的错误

然后测试发现在classB的定义后面一移动namespace std {...},错误就消失了

我看了编译器生成的汇编代码,发现GetMap在main.cpp和b.cpp中生成的名字是不一样的:

main.asm:

GetMap@B@@QEBAAEBV?$unordered_map@UA@@HV?$hash@UA@@@std@@U?$equal_to@UA@@@3@V?$allocator@U?$pair@$$CBUA@@H@std@@@3@@std@@XZ

b.asm:

GetMap@B@@QEBAAEBV?$unordered_map@UA@@HU?$hash@UA@@@std@@U?$equal_to@UA@@@3@V?$allocator@U?$pair@$$CBUA@@H@std@@@3@@std@@XZ

我查找了 MSVC 的 name mangling rules,发现 U 对应 structV 对应 class。所以我把template<> class hash<A>的定义改成了template<> struct hash<A>。然后错误就消失了。

我认为在专业化中使用 class 关键字是合法的,但我在标准中找不到对此的描述。

不过,我觉得问题可能没那么简单。这里的一个关键问题是 B.cpp 中 std::hash 的特化出现在 class B 的定义之后,而 main.cpp 中的顺序完全颠倒了。我认为这违反了 ODR,应该会导致未定义的行为。这也是为什么调换头文件顺序后程序正确的原因(使其与B.cpp中的顺序一致)。

我查了一些资料,找不到问题的标准描述:在B.cpp中,GetMap的声明不会导致unordered_map被实例化,但是它在函数定义中实例化。在声明和定义中间插入了 std::hash 的特化,这可能导致 unordered_map 看到 std::hash 的不同定义。编译器能看到这种特化吗?编译器应该选择这个专业吗?如果编译器可以看到特化,为什么在生成的汇编代码中使用主模板? (B.cpp中的compiler-generated名称使用“U”,代表struct