【问题标题】:const variables in header file and static initialization fiasco头文件中的 const 变量和静态初始化失败
【发布时间】:2011-01-26 15:44:28
【问题描述】:

在阅读了很多关于静态变量初始化的问题后,我仍然不确定这如何适用于命名空间级别的 const 变量。

我在构建脚本生成的 header 文件config.h 中有以下代码:

static const std::string path1 = "/xyz/abc";
static const std::string path2 = "/etc";

根据我阅读的内容,static 关键字不是必需的,甚至在此处已弃用。

我的问题:上面的代码是否容易出现静态初始化失败?

如果我在 header 文件中有以下内容myclass.h:

class MyClass
{
public:
    MyClass(const std::string& str) : m_str(str) {}
    std::string Get() const { return m_str; }

private:
    std::string m_str;
}

const MyClass myclass1("test");

这会对静态初始化造成任何问题吗?

如果我理解正确,由于const 变量具有内部链接,这两种情况都应该没有问题?

编辑:(由于 dribeas 的回答)

也许我应该提到我对以下用例感兴趣:

main.cpp:

#include <config.h>
#include <myclass.h>

std::string anotherString(path1 + myclass1.Get());

int main()
{
    ...
}

关于这个用例的另一个问题:在这种情况下编译器会优化掉path2吗?

【问题讨论】:

    标签: c++ initialization linkage


    【解决方案1】:

    您的第一个定义将path1 放置在包含config.h 的每个编译单元中。为避免这种情况,请不要在头文件中定义变量。通常你会将标头中的变量声明为extern:

    extern const std::string path1;
    extern const MyClass myclass1;
    

    并在翻译单元中定义它们,例如config.cpp:

    const std::string path1 = "/xyz/abc";
    const MyClass myclass1("test");
    

    有时您需要一个只能在一个翻译单元中使用的常量变量。然后你可以在文件范围内将该变量声明为static

    static const std::string path1 = "/xyz/abc";
    

    static 不再被弃用。 staticextern 有时是隐含的,但我总是忘记在哪里以及如何使用,所以我通常为所有命名空间级变量显式指定它们。

    【讨论】:

    • static不能只用在实现文件中。
    • 当然,C++中根本没有实现文件的概念,我试试换个说法
    • 我认为您正在寻找的短语是“翻译单元”而不是实现文件。值应在标题中声明并在最多一个翻译单元中定义。在这种情况下,由于它们是 const 并且具有隐含的内部链接,因此在链接时不会出现多重定义的符号错误,但是在包含此标头的每个翻译单元中确实定义了相同的符号。由于它们具有内部链接,因此它们不会中断链接,但编译器可能不会消除重复项。我曾经通过从头文件中删除字符串的定义,从一个可执行文件中减少了 8 MB。
    【解决方案2】:

    我试图从 C++03 标准文档中获取必要的信息。这是我发现的:

    关于const static 声明:

    根据第 3.5.3 节,在命名空间级别定义并声明为const 的对象默认具有内部链接static 还声明了一个命名空间级对象以具有内部链接,因此无需声明对象static const

    也根据附件 D.2

    static关键字的使用是 在声明对象时不推荐使用 命名空间范围(见 3.3.5)。

    关于静态初始化惨败:

    由于变量是在头文件中定义的,因此它们总是在使用它们的任何其他静态对象之前定义。

    来自第 3.6.2.1 节:

    具有静态存储持续时间的对象 在相同的命名空间范围内定义 翻译单元和动态 初始化应在初始化 它们的定义顺序 出现在翻译单元中。

    答案 1: 这意味着将变量传递到静态对象构造器应该没问题。

    答案 2: 但是,如果变量是从静态对象的非内联构造函数中引用的,则可能会出现问题:

    3.6.2.1和3.6.2.3都没有规定不同编译单元中的静态对象的初始化顺序if动态初始化是在main的第一条语句之前完成的。

    考虑以下几点:

    // consts.h
    #include <string>
    
    const std::string string1 = "ham";
    const std::string string2 = "cheese";
    
    // myclass.h
    #include <string>
    
    class MyClass
    {
    public:
        MyClass();
        MyClass(std::string str);
        std::string Get() { return memberString; }
    private:
        std::string memberString;
    }
    
    // myclass.cpp
    #include "consts.h"
    #include "myclass.h"
    
    MyClass::MyClass() : memberString(string1) {}
    
    MyClass::MyClass(std::string str) : memberString(str) {}
    
    // main.cpp
    #include <iostream>
    #include "consts.h"
    #include "myclass.h"
    
    MyClass myObject1;
    MyClass myObject2(string2);
    
    using namespace std;
    
    int main()
    {
        cout << myObject1.Get(); // might not print "ham"
        cout << myObject2.Get(); // will always print "cheese"
    }
    

    由于myclass.cpp 拥有自己的const 变量副本,因此在调用MyClass::MyClass() 时这些变量可能不会被初始化。

    所以是的,在头文件中定义的const 变量可以以一种容易导致静态初始化失败的方式使用

    据我所知,这仅适用于不需要静态初始化的变量:

    来自 C++03 标准,第 3.6.2.1 节:

    具有静态的 POD 类型 (3.9) 的对象 初始化的存储持续时间 常量表达式 (5.19) 应为 在任何动态之前初始化 初始化发生。

    【讨论】:

      【解决方案3】:

      当一个命名空间级别变量依赖于分配给不同命名空间级别变量的值时,所谓的静态初始化失败是一个问题,该变量之前可能会或未初始化。在您的两个示例中,没有这种依赖关系,应该没有任何问题。

      另一方面,这很容易出现这种类型的错误:

      // header.h
      extern const std::string foo;
      
      // constant.cpp
      const std::string foo( "foo" );
      
      // main.cpp
      #include "header.h"
      const std::string foobar( foo+"bar" );
      int main() {
         std::cout << foobar << std::endl;
      }
      

      不能保证foo 将在foobar 之前初始化,即使两者都是常量。这意味着程序行为未定义,它可以很好地打印“foobar”、“bar”或死亡。

      【讨论】:

      • 我在原始问题中更新了我的用例。这正是我想知道的场景。
      • 我不敢对此发表评论。我的第一个想法是它应该没问题(常量是翻译单元的本地变量,并且在第二个常量之前定义),但如果可能的话,我会避免这种结构。我知道可以确定的最简单的方法是在内部使用带有静态变量而不是全局变量的函数:inline myclass&amp; global_object() { myclass instance; return instance; } 该语言保证instance 将在第一次调用该函数时完全初始化,所以std::string other( global_object().Get() );肯定会工作......
      • 无论如何,我真的会尝试完全避免它,并在初始化时消除命名空间级别的变量。命名空间级别变量的初始化并非微不足道,它发生在两次传递中,其中从常量实例化的所有 globals 首先获取它们的值,然后在第二次传递中依赖于非常量的所有内容将根据定义的顺序(在同一个翻译单元内)进行初始化,当多个翻译单元链接到一个程序时,按未定义的顺序进行初始化。
      • @quant_dev:单独编译很可能是罪魁祸首。在 Python 中,你必须包含,并且事情的执行完全按照它们的解释。您只能引用已定义的内容。在 Java 中,您针对生成的 .class 构建源文件,这再次为目标文件提供了清晰的层次结构。在 C 或 C++ 中,您针对 声明 构建单独的翻译单元,链接器不知道如何对它们进行排序。更不用说可能没有顺序,你可以(即使你不应该)在 TU 之间有循环依赖
      • @quant_dev:这很容易说...怎么样?我相信,模块将在这个方向上有所帮助,但您必须以完全不同的模式构建,以抑制循环依赖(即头文件与实现文件),并有一种机制来跟踪什么依赖于其他什么。模块在这一行,但它们不会阻止问题,您将不得不忍受它以实现向后兼容性。 C++ 几乎完全向后兼容,这是标准委员会的核心标准之一。每个重大更改都经过仔细权衡:有多少人会受到影响以及修复的内容是什么
      【解决方案4】:

      静态初始化惨败是指相互依赖的静态变量。仅仅定义一些static const 变量不会成为问题的根源。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-04-19
        • 2021-05-11
        • 2019-04-15
        • 1970-01-01
        • 2012-12-15
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多