为什么这个包含顺序会导致 unordered_map 上的链接错误?

Dun*_*ndo 5 c++ include cl c++17

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

// 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();
}
Run Code Online (Sandbox Code Playgroud)

通过这个例子,我得到以下错误:

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
Run Code Online (Sandbox Code Playgroud)

但如果在main.cppI include A.hafter中B.h,则程序编译成功。有人可以解释为什么吗?

我花了很长时间才在实际代码中找到问题,有一些方法可以让我轻松理解该错误与包含顺序有关吗?

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

std::unordered_map<A, int>如果我更改withstd::unordered_set<A>但不更改std::map<A, int>and ,也会发生错误std::set<A>,所以我认为哈希存在一些问题。

正如所建议的,包含A.hinB.h而不是向前声明 A 可以使构建成功,而无需修改 中的包含顺序main.cpp

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

Plu*_*uto 3

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

首先,我将 Ah 和 Bh 的内容复制到 main.cpp 中并删除该#include指令。编译后,我仍然遇到同样的错误。

然后我测试发现,一旦我移动namespace std {...}到 class 的定义之后B,错误就消失了。

我阅读了编译器生成的汇编代码,发现main.cpp和b.cpp中生成的名称GetMap不同:

主.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 的名称修改规则并找到了UforstructVfor 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 中编译器生成的名称使用“U”,即 for struct