I am trying to use an unordered_map with a custom struct a key.
I know from other posts, that I need to define a custom hash function and comparison function.
My problem is the following:
Compilation works with no error for STATIC
and SHARED
library.
But on executing test_exec
I get the following error when I built a STATIC
library:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==19573==ERROR: AddressSanitizer: FPE on unknown address 0x561e54882917 (pc 0x561e54882917 bp 0x7ffea9e6f580 sp 0x7ffea9e6f580 T0)
#0 0x561e54882917 in std::__detail::_Mod_range_hashing::operator()(unsigned long, unsigned long) const (/home/TEST/minimal_example/build_DEBUG/test_exec+0x4917)
#1 0x561e5488855f in std::__detail::_Hash_code_base<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, test::MyEnum>, std::__detail::_Select1st, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, true>::_M_bucket_index(unsigned long, unsigned long) const (/home/TEST/minimal_example/build_DEBUG/test_exec+0xa55f)
AddressSanitizer:DEADLYSIGNAL
AddressSanitizer: nested bug in the same thread, aborting
How do I make it work for a STATIC
library?
test_lib.h
#ifndef TEST_LIB_H
#define TEST_LIB_H
#include <string>
#include <unordered_map>
namespace test {
enum class MyEnum { A, B, C, D };
using enum MyEnum;
const std::unordered_map<std::string, MyEnum> string_to_enum = {
{"A", A},
{"B", B},
{"C", C},
{"D", D},
};
const std::unordered_map<MyEnum, std::string> enum_to_string = {
{A, "A"},
{B, "B"},
{C, "C"},
{D, "D"},
};
struct MyStruct {
MyEnum my_enum;
unsigned int num;
bool operator==(const MyStruct &other) const {
return (my_enum == other.my_enum) && (num == other.num);
}
};
struct MyStructHash {
std::size_t operator()(const MyStruct &s) const {
return std::hash<MyEnum>{}(s.my_enum) ^
(std::hash<unsigned int>{}(s.num) << 1);
}
};
using MyMap = std::unordered_map<MyStruct, double, MyStructHash>;
MyMap get_map();
const MyMap MAP = get_map();
} // namespace test
#endif
test_lib.cpp
#include "test_lib.h"
namespace test {
MyMap get_map() {
MyMap map;
map[MyStruct(string_to_enum.at("A"), 2)] = 4.5;
return map;
}
} // namespace test
test_exec.cpp
#include "test_lib.h"
#include <iostream>
using namespace test;
int main(int ac, char **av) {
auto test = string_to_enum.at("A");
std::cout << enum_to_string.at(test) << std::endl;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
############################
# project name and version #
############################
project( Test VERSION 0.1 LANGUAGES CXX)
##############################
# set c++ flags and standard #
##############################
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -O0 -Wall -Wextra -Wpedantic -Wconversion -Wdouble-promotion -Wno-unused-parameter -Wno-unused-function -Wno-sign-conversion -fsanitize=address")
add_library(test_lib STATIC test_lib.cpp)
set_target_properties(test_lib PROPERTIES PUBLIC_HEADER test_lib.h)
add_executable(test_exec test_exec.cpp)
target_link_libraries(test_exec PUBLIC
test_lib
)
So far I have constructed this minimal example to rule out any other problems in my code.
EDIT:
After defining
extern const MyMap MAP;
in test_lib.h and
const MyMap MAP = get_map();
in test_lib.cpp it works.
Here's what happens. const
global variables are implicitly also static
(unless explicitly marked extern
, which in your example they are not). Thus, every translation unit that includes test_lib.h
gets its own instance of string_to_enum
, enum_to_string
and MAP
.
The order of initialization of globals within each translation unit is sequential, top to bottom. But the order of initialization between different translation units is undefined. Which may lead to the so-called static initialization order fiasco - which is what you observe.
It so happens that, in your example, the three global variables associated with test_exec
translation unit get initialized first. The initializer of MAP
calls get_map()
, which attempts to use string_to_enum
. But get_map()
is located in test_lib
translation unit, and so is referring to the instance of string_to_enum
associated with that unit - which hasn't been initialized yet. Whereupon your program exhibits undefined behavior by way of using an object before its lifetime has started.
The solution is to declare global variables with extern
in the header file, and define them in exactly one source file; so there's only one instance of each in the program. (Better still, if possible, avoid global variables entirely.)