3

我有一堆类有一个静态成员,它是一个枚举值。我在其他地方有一张地图,以这个枚举为键。现在,如果我在函数中使用模板参数来访问地图,我会得到一个未定义的引用。

为了清楚起见,这是一个简化的非工作示例:

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

有人可以向我解释这种行为吗?

4

2 回答 2

7

使用时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-used (3.2) 并且命名空间范围定义不应包含初始化程序,则该成员仍应在命名空间范围内定义。

[C++11: 3.2/2]: [..]名称显示为潜在求值表达式的变量是 odr-used 除非它是满足出现在常量表达式 (5.19) 中的要求并且立即进行左值到右值转换 (4.1) 的对象应用。[..]

于 2013-05-24T08:10:51.217 回答
3

那是因为通过引用std::map::operator[]来获取它的参数,这使得你的变量odr-used(参见 C++11 标准的第 3.2/3 段)。

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

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

template<int T>
const int A::Type;

根据 C++11 标准的第 9.4.2/3 段:

如果非易失性const静态数据成员是整数或枚举类型,则其在类定义中的声明可以指定一个大括号或相等初始化器,其中作为赋值表达式的每个初始化器子句 都是一个常量表达式 (5.19) . [ ... ]如果成员在程序中被 odr-used (3.2) 并且命名空间范围定义不应包含初始化器,则该成员仍应在命名空间范围内定义

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

于 2013-05-24T08:10:54.630 回答