C++保证编译单元(.cpp文件)中的变量按声明顺序初始化。对于多个编译单元,此规则适用于每个单独的编译单元(我指类外的静态变量)。
但是,跨不同编译单元的变量初始化顺序是未定义的。
在哪里可以看到有关gcc和MSVC的初始化顺序解释(我知道依赖它是一个非常糟糕的想法 - 这只是为了理解当将遗留代码移动到新的GCC主要版本和不同的操作系统时可能遇到的问题)?
C++保证编译单元(.cpp文件)中的变量按声明顺序初始化。对于多个编译单元,此规则适用于每个单独的编译单元(我指类外的静态变量)。
但是,跨不同编译单元的变量初始化顺序是未定义的。
在哪里可以看到有关gcc和MSVC的初始化顺序解释(我知道依赖它是一个非常糟糕的想法 - 这只是为了理解当将遗留代码移动到新的GCC主要版本和不同的操作系统时可能遇到的问题)?
正如你所说,订单在不同的编译单元之间是未定义的。
在同一编译单元中,顺序是明确定义的:与定义的顺序相同。
这是因为这不是在语言级别上解决的,而是在链接器级别上解决的。因此,您确实需要查看链接器文档。但我真的怀疑这样做会有任何有用的帮助。
对于gcc:请查看ld
我发现即使更改链接的对象文件的顺序也会更改初始化顺序。因此,您不仅需要担心链接器,还需要担心构建系统如何调用链接器。即使尝试解决问题也几乎是不可能的。
这通常只在初始化其自身初始化期间引用彼此的全局变量时才会出现问题(因此仅影响具有构造函数的对象)。
main()
之前初始化的静态存储期变量。我认为模块之间的构造函数顺序主要取决于将对象按照什么顺序传递给链接器。
然而,GCC允许您使用 init_priority
来显式指定全局构造函数的顺序:
class Thingy
{
public:
Thingy(char*p) {printf(p);}
};
Thingy a("A");
Thingy b("B");
Thingy c("C");
输出 'ABC' ,正如你所期望的那样,但是
Thingy a __attribute__((init_priority(300))) ("A");
Thingy b __attribute__((init_priority(200))) ("B");
Thingy c __attribute__((init_priority(400))) ("C");
输出'BAC'。
init
,fini
或 init_priority
。相反,使用 constructor
属性。还有一个 destructor
属性。你可能需要在其他编译器和平台上使用 init
和 fini
,但对于 GCC,你不需要使用它。另请参阅 GCC 邮件列表上的 *"Clarification of attribute init_priority"*。 - jwwconstructor
和destructor
并不相关:它们存在是为了在main()
之前和之后强制调用某些函数。它们与特定对象的初始化顺序无关,对于这一点,init_priority
是恰当的工具。安德鲁看到的警告可能是因为他使用了一个不符合init_priority
文档规定的“101到65535之间(包括边界)”的值;因此可以得出结论,超出该范围的值是被保留的,正如警告所清楚地表明的那样。我认为这两件事与问题无关。 - underscore_d既然您已经知道除非绝对必要,否则不应依赖此信息,那么它来了。 我在各种工具链(MSVC、gcc / ld、clang / llvm等)中的一般观察是,将目标文件传递给链接器的顺序就是它们初始化的顺序。
虽然有例外,但我不宣称全部知道,但这里是我自己遇到的例子:
1)GCC 4.7之前的版本实际上以与链接行相反的顺序进行初始化。 GCC中的此票 就是变化发生的地方,并且它破坏了很多依赖于初始化顺序的程序(包括我的!)。
2)在GCC和Clang中,使用构造函数优先级可以更改初始化顺序。请注意,这仅适用于声明为“构造函数”的函数(即它们应该像全局对象构造函数一样运行)。我尝试过像这样使用优先级,并发现即使在构造函数的最高优先级下,所有没有优先级的构造函数(例如普通全局对象、没有优先级的构造函数)也将首先初始化。换句话说,优先级只是相对于具有优先级的其他函数,但真正的一等公民是没有优先级的那些。更糟糕的是,由于上述第1点,在GCC 4.7之前实际上相反。
3) 在Windows上,存在一个非常简洁实用的共享库(DLL)入口函数叫做DllMain()。如果定义了这个函数,它将在全局数据初始化完成之后直接运行,参数"fdwReason"等于DLL_PROCESS_ATTACH,而在消费应用程序有机会调用DLL上的任何函数之前运行。这在某些情况下非常有用,在GCC或Clang中使用C或C++时绝对没有类似的行为。最接近的方法是使用优先级构造函数(参见上面的第2点),但这绝对不是相同的东西,并且对于许多DllMain()适用的用例无法工作。
4) 如果你正在使用CMake生成你的构建系统(我经常使用),我发现输入源文件的顺序将是它们产生的目标对象文件在链接器中的顺序。然而,通常情况下,您的应用程序/DLL还会链接其他库,在这种情况下,那些库将被放置于链接线路的源文件之后。如果你想让其中一个全局对象成为第一个进行初始化的对象,那么你很幸运,只需将包含该对象的源文件放在源文件列表的第一个位置即可。但如果你想让其中一个全局对象成为最后一个进行初始化的对象(这实际上可以复制DllMain()的行为!),那么你可以调用add_library()函数将该源文件生成一个静态库,并将生成的静态库作为应用程序/DLL的target_link_libraries()调用中的最后一个链接依赖项添加。请注意,此情况下全局对象可能会被优化掉,你可以使用--whole-archive标志来强制链接器不要删除该特定小型存档文件中未使用的符号。
结束语
要完全了解链接应用程序/共享库的初始化顺序,请将--print-map传递给ld链接器,并通过grep查找.init_array(或在GCC 4.7之前,查找.ctors)。每个全局构造函数将按其初始化顺序打印出来,并且请记住,在GCC 4.7之前,顺序是相反的(参见上面的第(1)点)。
编写此答案的动机是我需要了解此信息,别无选择,只能依靠初始化顺序,并发现其他SO帖子和互联网论坛中只有零星的信息。其中大部分是通过大量实验学习的,我希望这可以为某些人节省时间!
一个健壮的解决方案是使用一个getter函数,该函数返回一个静态变量的引用。下面是一个简单的示例,在我们的SDG Controller中间件中有一个复杂的变体。
// Foo.h
class Foo {
public:
Foo() {}
static bool insertIntoBar(int number);
private:
static std::vector<int>& getBar();
};
// Foo.cpp
std::vector<int>& Foo::getBar() {
static std::vector<int> bar;
return bar;
}
bool Foo::insertIntoBar(int number) {
getBar().push_back(number);
return true;
}
// A.h
class A {
public:
A() {}
private:
static bool a1;
};
// A.cpp
bool A::a1 = Foo::insertIntoBar(22);
bool A::a1
开始。然后调用Foo::insertIntoBar(22)
,这将调用Foo::getBar()
,在返回初始化对象的引用之前,静态std::vector<int>
变量的初始化将会发生。static std::vector<int> bar
直接作为Foo类
的成员变量放置,取决于源文件的命名顺序,存在bar
在insertIntoBar()
被调用后初始化从而导致程序崩溃的可能性。insertIntoBar()
,则它们的顺序不依赖于源文件的名称,即随机的,但是std::vector<int>
将保证在任何值被插入之前初始化。
constexpt
基本上是另一个编译器时间常量))。这不应该导致关于此事的任何争论发生变化或导致问题出现。你可以提出一个问题,以便我们可以更详细地探讨。 - Martin York