C ++静态成员初始化(模板内部有趣)

C++ Static member initialization (template fun inside)

对于静态成员初始化,我使用一个嵌套的helper结构,它对非模板化的类很好。但是,如果封闭类是由模板参数化的,那么如果主代码中没有访问助手对象,则嵌套的初始化类不会被实例化。为了举例说明,一个简化的例子(在我的例子中,我需要初始化一个向量)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <string>
#include <iostream>

struct A
{
    struct InitHelper
    {
        InitHelper()
        {
            A::mA ="Hello, I'm A.";
        }
    };
    static std::string mA;
    static InitHelper mInit;

    static const std::string& getA(){ return mA; }
};
std::string A::mA;
A::InitHelper A::mInit;


template<class T>
struct B
{
    struct InitHelper
    {
        InitHelper()
        {
            B<T>::mB ="Hello, I'm B."; // [3]
        }
    };
    static std::string mB;
    static InitHelper mInit;

    static const std::string& getB() { return mB; }
    static InitHelper& getHelper(){ return mInit; }
};
template<class T>
std::string B<T>::mB; //[4]
template<class T>
typename B<T>::InitHelper B<T>::mInit;


int main(int argc, char* argv[])
{
    std::cout <<"A =" << A::getA() << std::endl;

//    std::cout <<"B =" << B<int>::getB() << std::endl; // [1]
//    B<int>::getHelper();    // [2]
}

使用G++4.4.1:

    百万千克1

    [1]和[2]评论:

    1
    A = Hello, I'm A.

    预期工程

    百万千克1百万千克1

    [1]未注释:

    1
    2
    A = Hello, I'm A.
    B =

    我希望initHelper初始化MB

    百万千克1百万千克1[1]和[2]未注释:

    1
    2
    A = Hello, I'm A.
    B = Hello, I'
    m B.

    。预期工程百万千克1百万千克1[1]评论,[2]未注释:[3]处静态初始化阶段的segfault百万千克1

因此,我的问题是:这是一个编译器错误,还是位于监视器和椅子之间?如果后者是这样:是否有一个优雅的解决方案(即不显式调用静态初始化方法)?

更新一:这似乎是一种期望的行为(如ISO/IEC C++ 2003标准,147.1所定义):

Unless a member of a class template or a member template has been explicitly instantiated or explicitly specialized, the specialization of the member is implicitly instantiated when the specialization is referenced in a context that requires the member definition to exist; in particular, the initialization (and any associated side-effects) of a static data member does not occur unless the static data member is itself used in a way that requires the definition of the static data member to exist.


一段时间前,我在Usenet上讨论过这个问题,当时我正试图回答另一个关于stackoverflow的问题:静态数据成员的实例化点。我认为减少测试用例并单独考虑每个场景是值得的,所以让我们先看一下更一般的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct C { C(int n) { printf("%d
"
, n); } };

template<int N>
struct A {
  static C c;
};

template<int N>
C A<N>::c(N);

A<1> a; // implicit instantiation of A<1> and 2
A<2> b;

您有静态数据成员模板的定义。由于14.7.1的原因,这还没有创建任何数据成员:

"... in particular, the initialization (and any associated side-effects) of a static data member does not occur unless the static data member is itself used in a way that requires the definition of the static data member to exist."

当实体被"使用"时,根据定义该词的一个定义规则(在3.2/2处),需要对某物(=实体)进行定义。特别是,如果所有引用都来自未实例化的模板、模板或sizeof表达式的成员或不"使用"实体的类似事物(因为它们不是潜在地对实体进行评估,或者它们只是不作为自身使用的函数/成员函数存在),则不会实例化此类静态数据成员。

14.7.1/7的隐式实例化实例化静态数据成员的声明——也就是说,它将实例化处理该声明所需的任何模板。但是,它不会实例化定义——也就是说,初始值设定项没有实例化,静态数据成员类型的构造函数也没有隐式定义(标记为已使用)。

这意味着,上面的代码还不会输出任何内容。现在让我们对静态数据成员进行隐式实例化。

1
2
3
4
int main() {
  A<1>::c; // reference them
  A<2>::c;
}

这将导致两个静态数据成员存在,但问题是-初始化顺序如何?简单地说,人们可能会认为3.6.2/1适用,即(我强调):

"Objects with static storage duration defined in namespace scope in the same translation unit and dynamically initialized shall be initialized in the order in which their definition appears in the translation unit."

现在,正如在usenet post中所说并在这个缺陷报告中解释的,这些静态数据成员不是在翻译单元中定义的,而是在实例化单元中实例化的,如2.1/1中所述:

Each translated translation unit is examined to produce a list of required instantiations. [Note: this may include instantiations which have been explicitly requested (14.7.2). ] The definitions of the required templates are located. It is implementation-defined whether the source of the translation units containing these definitions is required to be available. [Note: an implementation could encode sufficient information into the translated translation unit so as to ensure the source is not required here. ] All the required instantiations are performed to produce instantiation units. [Note: these are similar to translated translation units, but contain no references to uninstantiated templates and no template definitions. ] The program is ill-formed if any instantiation fails.

这样一个成员的实例化点也并不真正重要,因为这样一个实例化点是一个实例化与其翻译单元之间的上下文链接——它定义了可见的声明(如14.6.4.1中指定的那样),并且每个实例化点必须给出与之相同的含义,如在3.2/5的一个定义规则中指定,最后一个项目符号)。

如果我们想要有序的初始化,我们必须进行安排,这样我们就不会弄乱实例化,而是使用显式声明——这是显式专门化的领域,因为它们与普通声明没有真正的不同。事实上,C++0X将EDOCX1×8的措辞改为:

Dynamic initialization of a non-local object with static storage duration is either ordered or unordered.
Definitions of explicitly specialized class template static data members have ordered initialization. Other
class template static data members (i.e., implicitly or explicitly instantiated specializations) have unordered initialization.

这对您的代码意味着:

  • [1][2]评论说:不存在对静态数据成员的引用,因此不实例化它们的定义(也不实例化它们的声明,因为不需要实例化B)。没有副作用。
  • [1]未注释:使用B::getB(),本身使用B::mB,要求静态成员存在。字符串在main之前初始化(在该语句之前的任何情况下,作为初始化非本地对象的一部分)。没有东西使用B::mInit,所以它没有实例化,也没有创建B::InitHelper的对象,这使得它的构造函数没有被使用,反过来,它也不会给B::mB赋值:您只会输出一个空字符串。
  • [1][2]未声明:这对你有效是运气(或相反)。如上所述,不需要特定的初始化调用顺序。它可能在vc++上工作,在gcc上失败,在clang上工作。我们不知道。
  • [1]评论,[2]未注释:同样的问题-同样,两个静态数据成员都被使用:B::mInitB::getHelper使用,B::mInit的实例化将导致其构造函数被实例化,这将使用B::mB-但是对于您的编译器,在这个特定的运行中顺序是不同的(非特殊的IED行为不需要在不同的运行之间保持一致:它首先初始化B::mInit,它将在尚未构造的字符串对象上操作。

问题是,为静态成员变量提供的定义也是模板。

1
2
3
4
template<class T>
std::string B<T>::mB;
template<class T>
typename B<T>::InitHelper B<T>::mInit;

在编译过程中,这实际上没有定义任何内容,因为T未知。它类似于类声明或模板定义,编译器在看到它时不会生成代码或保留存储。

定义在稍后使用模板类时隐式地发生。因为在SegFaulting情况下,您不使用b::minit,所以永远不会创建它。

解决方案将明确定义所需的成员(而不初始化它):将源文件

1
2
template<>
typename B<int>::InitHelper B<int>::mInit;

这与显式定义模板类的工作方式基本相同。


  • [1]未注案例:没关系。static InitHelper B::mInit不存在。如果不使用模板类(结构)的成员,则不会编译它。

  • [1]和[2]未注案例:没关系。B::getHelper()使用static InitHelper B::mInit存在,mInit存在。

  • [1]评论,[2]未注释:它在VS2008中为我工作。