c++模板类静态const变量成员作为映射键给出未定义引用

C++ template class static const variable member as map key gives undefined reference

本文关键字:映射 引用 未定义 成员 静态 变量 const c++      更新时间:2023-10-16

我有一堆类,有一个静态成员是枚举值。我在别处有一个映射,这个enum是键。现在,如果我在函数中使用模板形参来访问map,我将得到一个未定义的引用。

为了说清楚,这里有一个简化的不工作的例子:

template<int T>
struct A
  {
    static const int Type = T;
  }
template<class T>
void fun()
  {
    cout << map_[T::Type] << endl;
  }
map<int, string> map_{{1337, "1337"}};
主:

fun<A<1337>();

给我(g++ 4.7):

undefined reference to `(anonymous namespace)::A<1337>::Type'

template<class T>
void fun()
  {
    auto key = T::Type;
    cout << map_[key] << endl;
  }

编译并打印1337

谁能给我解释一下这种行为?

当您使用T::Type时,您必须定义它:

template<int T>
struct A
{
   static const int Type = T;
}
template <int T>
const int A<T>::Type;

是的,即使你在A<T>内提供了它的内联初始化器!

你可能没有意识到这一点的原因,与你在第二种情况下不会遇到同样问题的原因是一样的—由于左值到右值的直接转换,该标准允许编译器优化在运行时引用Type的需求,而不是在编译时挑选值。链接器不需要搜索定义,也不会得到错误。


[C++11: 9.4.2/2]: 静态数据成员在其类定义中的声明不是定义,并且可以是不完整类型,而不是cv限定的void。静态数据成员的定义应该出现在包含该成员的类定义的命名空间作用域中。在命名空间作用域的定义中,静态对象的名称数据成员应使用::操作符由其类名限定。静态数据成员定义中的初始化表达式位于其类的作用域中(3.3.7)。 [. .]

[C++11: 9.4.2/3]: 如果非易失性const static数据成员是整型或枚举类型,则其在类定义中的声明可以指定大括号或相等初始化器,其中每个初始化器子句都是赋值表达式是常量表达式(5.19)。文字类型的static data成员可以用constexpr说明符在类定义中声明;如果是,其声明应指定大括号或等号初始化式,其中每个为赋值表达式的初始化式子句都是常量表达式。[注意:在这两种情况下,成员都可以出现在常量表达式中。 如果成员在程序中被odr(3.2)使用,则该成员仍应在命名空间作用域中定义,并且命名空间作用域定义不应包含初始化式。

[C++11: 3.2/2]: […]如果变量名出现在潜在求值表达式中,则不使用该变量,除非它是一个对象,满足出现在常量表达式(5.19)中的要求,并且立即应用左值到右值的转换(4.1)。 [. .]

这是因为std::map::operator[]通过引用获取其参数,这使得您的变量odr使用(参见c++ 11标准的3.2/3段)。

简而言之,整个事情归结为这样一个事实:当编译器需要绑定一个对象的引用时,它需要知道对象的地址,这使得它不可能将该对象视为纯值并执行内联。

在这种情况下,您需要在全局命名空间范围内提供静态数据成员的定义,以便编译器知道该对象占用的存储区域(即它的地址是什么):
template<int T>
const int A::Type;

c++ 11标准第9.4.2/3段:

如果非易失性const静态数据成员是整型或枚举类型,则在类中声明定义可以指定大括号或等号初始化式,其中每个初始化式子句都是赋值表达式是常量表达式(5.19)。[…成员仍然被定义在一个命名空间作用域中,如果它在程序中被odr(3.2)使用,并且命名空间作用域定义不允许使用包含一个初始化式

另一方面,在程序的第一个版本中,您只使用静态数据成员的,这意味着Type没有被odr使用,并且不需要在命名空间范围内定义。