42

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

#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] 和 [2] 评论说:

    A = 你好,我是 A。

    按预期工作

  • [1] 未注释:

    A = 你好,我是 A。
    乙 =

    我希望 InitHelper 初始化 mB

  • [1] 和 [2] 未注释:
    A = 你好,我是 A。
    B = 你好,我是 B。
    按预期工作
  • [1] 已评论,[2] 未评论:
    [3] 的静态初始化阶段的 Segfault

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

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

除非类模板或成员模板的成员已被显式实例化或显式特化,否则当在需要成员定义存在的上下文中引用特化时,成员的特化将被隐式实例化;特别是,静态数据成员的初始化(以及任何相关的副作用)不会发生,除非该静态数据成员本身的使用方式要求该静态数据成员的定义存在。

4

3 回答 3

39

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


struct C { C(int n) { printf("%d\n", 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

“......特别是,静态数据成员的初始化(以及任何相关的副作用)不会发生,除非静态数据成员本身的使用方式需要存在静态数据成员的定义。”

根据定义该词 (at 3.2/2) 的单一定义规则,当“使用”该实体时,需要对该事物 (= 实体) 进行定义。特别是,如果所有引用都来自未实例化的模板,则模板或sizeof表达式的成员或不“使用”实体的类似事物(因为它们要么没有潜在地评估它,要么它们还不作为函数存在/member 函数本身使用),这样的静态数据成员不会被实例化。

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

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

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

这会导致两个静态数据成员存在,但问题是——初始化的顺序是怎样的?在简单的阅读中,人们可能会认为这是3.6.2/1适用的,它说(我强调):

“在同一翻译单元中的命名空间范围内定义的具有静态存储持续时间并动态初始化的对象应按照其定义在翻译单元中出现的顺序进行初始化。”

现在正如 usenet 帖子中所说并在此缺陷报告中解释的那样,这些静态数据成员没有在翻译单元中定义,而是在实例化单元中实例化,如下所述2.1/1

检查每个翻译的翻译单元以生成所需实例化的列表。[注意:这可能包括已明确请求的实例化(14.7.2)。] 所需模板的定义位于。是否需要包含这些定义的翻译单元的源是由实现定义的。[注意:实现可以将足够的信息编码到翻译后的翻译单元中,以确保此处不需要源。] 执行所有必需的实例化以生成实例化单元。[注意:这些类似于翻译的翻译单元,但不包含对未实例化模板的引用和模板定义。] 如果任何实例化失败,则程序是非良构的。

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

如果我们想要有序的初始化,我们必须进行安排,这样我们就不会弄乱实例化,而是使用显式声明 - 这是显式专业化的领域,因为这些与普通声明并没有真正的不同。事实上,C++0x 将其措辞改为3.6.2如下:

具有静态存储持续时间的非本地对象的动态初始化是有序的或无序的。显式专门化的类模板静态数据成员的定义已排序初始化。其他类模板静态数据成员(即,隐式或显式实例化的特化)具有无序初始化。


这对您的代码意味着:

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

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

template<class T>
std::string B<T>::mB;
template<class T>
typename B<T>::InitHelper B<T>::mInit;

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

当您使用模板类时,该定义会隐式发生。因为在段错误的情况下你不使用 B<int>::mInit,它永远不会被创建。

一个解决方案是明确定义所需的成员(不初始化它):将源文件放在某处

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

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

于 2009-11-30T11:32:51.750 回答
2
  • [1] 未注释案例:没关系。static InitHelper B<int>::mInit不存在。如果不使用模板类(结构)的成员,则不会编译。

  • [1] 和 [2] 未注释的情况:没关系。B<int>::getHelper()使用static InitHelper B<int>::mInitmInit存在。

  • [1] 已评论,[2] 未评论:它在 VS2008 中适用于我。

于 2009-11-30T11:15:27.903 回答