为什么模板 class 的内联静态变量没有初始化?
Why is an inline static variable of a template class not initialized?
最小的例子和问题
在这个例子中,非模板class的内联静态成员变量c2
在实例化class成员时初始化,而成员变量c1
模板 class 不是。有什么区别?为什么 c1
没有被初始化,除非我通过获取它的地址强制它被初始化,而 c2
是无条件初始化的?
struct C1 {
C1() { std::cerr << "C1()\n"; }
};
struct C2 {
C2() { std::cerr << "C2()\n"; }
};
template<typename T>
struct Template {
inline static C1 c1;
};
struct Nontemplate {
inline static C2 c2;
};
int main() {
Template<int> a;
Nontemplate b;
(void)a;
(void)b;
}
// Output:
C2()
上下文和推理
这里有一些关于最小示例的上下文。我有 Nontemplate
class 继承自 Template<something>
,c2
的构造函数依赖于 c1
。我希望在 c2
之前创建 c1
;然而,事实并非如此。
template<typename T>
struct Template {
inline static C1 c1;
};
struct Nontemplate : public Template<int> {
struct C2 {
C2() {
std::cerr << "Do something with Nontemplate::C1\n";
std::cerr << "&Nontemplate::c1 = " << &Nontemplate::c1 << "\n";
}
};
inline static C2 c2;
};
int main() {
Nontemplate b;
(void)b;
}
// Output:
Do something with Nontemplate::C1
&Nontemplate::c1 = 0x600ea8
C1()
代码是使用带有 -std=c++17
标志的 g++ 7.2 编译的。 -O0
和 -O2
给出相同的结果。
class 模板的隐式实例化只会导致其包含的声明的实例化。定义通常仅在需要定义存在的上下文中使用时才会实例化。
因此,只要您不以需要其定义存在的方式使用 Template<int>::c1
(即通过 odr-使用它),那么它就根本不会被定义。
如您所述,使用变量的一种方法是获取其地址。
即使您强制实例化变量,也无法保证它何时会被初始化。
C1
的构造函数不是constexpr
所以Nontemlate::c1
的初始化不能是常量表达式。这意味着您将获得 Template<int>::c1
的动态初始化。作为模板特化的一部分的全局静态变量的动态初始化是无序,这意味着不能保证它们相对于全局静态变量的任何其他动态初始化将以什么顺序发生。
同样Nontemlate::c2
不是由常量表达式初始化的,所以也是动态初始化的。尽管 Nontemlate::c2
具有 部分排序的动态初始化 (作为 inline
变量而不是模板特化的一部分),它仍然相对于 Template<int>::c1
不确定排序如上所述。
也没有严格要求在输入main
之前初始化Template<int>::c1
和Nontemlate::c2
。初始化是否推迟到以后,但在相应变量的第一次 ODR 使用之前,由实现定义。不过,我认为这种延迟主要用于运行时动态库加载。
避免全局静态存储持续时间变量排序问题的常用方法是使用返回对局部静态变量的引用的函数,即:
template<typename T>
struct Template {
static auto& c1() {
static C1 instance;
return instance;
}
};
尽管这在经常调用时可能会对性能产生影响,因为每次都必须检查局部静态的构造。
或者,如果初始化器可以是一个常量表达式,使变量constexpr
应该保证常量初始化,这意味着根本不会发生动态初始化并且不会出现订购问题。
最小的例子和问题
在这个例子中,非模板class的内联静态成员变量c2
在实例化class成员时初始化,而成员变量c1
模板 class 不是。有什么区别?为什么 c1
没有被初始化,除非我通过获取它的地址强制它被初始化,而 c2
是无条件初始化的?
struct C1 {
C1() { std::cerr << "C1()\n"; }
};
struct C2 {
C2() { std::cerr << "C2()\n"; }
};
template<typename T>
struct Template {
inline static C1 c1;
};
struct Nontemplate {
inline static C2 c2;
};
int main() {
Template<int> a;
Nontemplate b;
(void)a;
(void)b;
}
// Output:
C2()
上下文和推理
这里有一些关于最小示例的上下文。我有 Nontemplate
class 继承自 Template<something>
,c2
的构造函数依赖于 c1
。我希望在 c2
之前创建 c1
;然而,事实并非如此。
template<typename T>
struct Template {
inline static C1 c1;
};
struct Nontemplate : public Template<int> {
struct C2 {
C2() {
std::cerr << "Do something with Nontemplate::C1\n";
std::cerr << "&Nontemplate::c1 = " << &Nontemplate::c1 << "\n";
}
};
inline static C2 c2;
};
int main() {
Nontemplate b;
(void)b;
}
// Output:
Do something with Nontemplate::C1
&Nontemplate::c1 = 0x600ea8
C1()
代码是使用带有 -std=c++17
标志的 g++ 7.2 编译的。 -O0
和 -O2
给出相同的结果。
class 模板的隐式实例化只会导致其包含的声明的实例化。定义通常仅在需要定义存在的上下文中使用时才会实例化。
因此,只要您不以需要其定义存在的方式使用 Template<int>::c1
(即通过 odr-使用它),那么它就根本不会被定义。
如您所述,使用变量的一种方法是获取其地址。
即使您强制实例化变量,也无法保证它何时会被初始化。
C1
的构造函数不是constexpr
所以Nontemlate::c1
的初始化不能是常量表达式。这意味着您将获得 Template<int>::c1
的动态初始化。作为模板特化的一部分的全局静态变量的动态初始化是无序,这意味着不能保证它们相对于全局静态变量的任何其他动态初始化将以什么顺序发生。
同样Nontemlate::c2
不是由常量表达式初始化的,所以也是动态初始化的。尽管 Nontemlate::c2
具有 部分排序的动态初始化 (作为 inline
变量而不是模板特化的一部分),它仍然相对于 Template<int>::c1
不确定排序如上所述。
也没有严格要求在输入main
之前初始化Template<int>::c1
和Nontemlate::c2
。初始化是否推迟到以后,但在相应变量的第一次 ODR 使用之前,由实现定义。不过,我认为这种延迟主要用于运行时动态库加载。
避免全局静态存储持续时间变量排序问题的常用方法是使用返回对局部静态变量的引用的函数,即:
template<typename T>
struct Template {
static auto& c1() {
static C1 instance;
return instance;
}
};
尽管这在经常调用时可能会对性能产生影响,因为每次都必须检查局部静态的构造。
或者,如果初始化器可以是一个常量表达式,使变量constexpr
应该保证常量初始化,这意味着根本不会发生动态初始化并且不会出现订购问题。