5

I had a problem with include order that I cannot explain. I will show you a minimal example with four files:

// 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();
}

With this example I get the following error:

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

But if in main.cpp I include A.h after B.h, the program compiles successfully. Can someone explain why?

It took me a long time to find the problem in real code, there is some method to make me understand easily that the error is related to include order?

Edit: I made some other test to investigate the problem.

The error occurs also if I change the std::unordered_map<A, int> with std::unordered_set<A> but not with std::map<A, int> and std::set<A>, so I think that there is some problem with the hash.

As suggested, including A.h in B.h instead of forward declaring A makes the build succeed without modifying the include order in main.cpp.

So I think that the question become: why forward declaring A, and thus having an incomplete type for the key of unordered map, causes the error?

Dundo
  • 714
  • 8
  • 12

1 Answers1

3

I tested the same code in Visual Studio 2022 and got the same error. After my exploration, I found the problem.

Firstly, I copied the contents of A.h and B.h into main.cpp and removed the #include directive. After compiling, I still got the same error.

Then I tested and found that as soon as I moved namespace std {...} after the definition of class B, the error went away.

I read the assembly code generated by the compiler and found that the names generated for GetMap in main.cpp and b.cpp are different:

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

I looked up MSVC's name mangling rules and found U for struct and V for class. So I change the definition of template<> class hash<A> to template<> struct hash<A>. Then the error disappeared.

I think it's legal to use class keyword instead in the specialization, but I can't find a description of this in the standard.

However, I think the problem may not be that simple. A key issue here is that the specialization of std::hash in B.cpp appears after the definition of class B, while in main.cpp the order is completely reversed. I think this violates the ODR and should result in undefined behavior. This is also why the program is correct after swapping the order of the header files (making it consistent with the order in B.cpp).

I looked up some sources and couldn't find a standard description of the problem: In B.cpp, the declaration of GetMap does not cause unordered_map to be instantiated, but it is instantiated in the function definition. A specialization of std::hash was inserted in the middle of declaration and definition, which could cause unordered_map to see a different definition of std::hash. Can the compiler see this specialization? Should the compiler choose this specialization? If the compiler can see the specialization, why is the primary template used in the generated assembly code? (The compiler-generated name in B.cpp uses "U", which is for struct)

Pluto
  • 910
  • 3
  • 11