在C++中,如何确保一个对象在另一个对象之前构建?

3
在C++中,我们有两个全局对象,它们分别在不同的文件中由其他人定义。其中一个对象的构建依赖于另一个对象已经被构建。
class independent; 
class  dependent;
independent o1;
dependent o2;

为了确保o2之前构建o1,我可以在o2之前声明o1。
这样做可以保证o1在o2之前被构建吗?如果编译器改变了顺序呢?
谢谢。

3
这篇文章讨论了C++中静态变量初始化的顺序问题。当一个程序中有多个源文件时,这些源文件中定义的静态变量的初始化顺序是未定义的,这可能导致程序出现不可预测的行为。作者提供了一些技术来规避这个问题,例如用函数来替代静态变量等。 - jamesdlin
7个回答

8
C++标准保证同一编译单元内静态变量的顺序初始化,但在不同的编译单元中顺序是未定义的。这通常被称为“静态初始化顺序问题”。因此,如果你的变量在同一个.cpp文件中声明,它们将按照你声明它们的顺序进行初始化,如果它们在不同的文件中,则根本不知道顺序(我见过非常激进的链接优化情况,它们无论何时都会更改静态变量初始化的顺序,而不考虑情况,但这不符合标准,并且在正常使用情况下不应该发生)。
核心问题:你的问题背后存在设计问题。如果一个类依赖于另一个类,则其结构应明确显示此依赖关系,并使得不可能出现未满足依赖关系的情况。如果你有一个全局对象依赖于另一个对象,但你对其没有明确的控制,则可能不应使它成为全局对象。由于你的问题没有提供足够的细节来了解哪种设计最适合你,因此以下是一些建议,供你做出决策。
依赖:首先,让我们表达这种依赖关系...
初始化时间依赖性:类dependent的一个实例需要一个independent对象进行初始化,但后来不再需要。那么只需使默认构造函数无法访问并定义你自己的构造函数即可。
class dependent
{
  private:
    dependent(); // Implementation is optional

  public:
    dependent(const independent& sister)
    {
      // Initialize stuff
    }
}

终身依赖

dependent 类的实例在其整个生命周期中需要 independent 对象才有意义。因此,需要定义一个成员变量引用另一个对象,并且将默认构造函数设为不可访问,并定义自己的构造函数。

class dependent
{
  private:
    dependent(); // No implementation

  public:
    dependent(const independent& sister): m_sister(sister)
    {
      // Initialize stuff
    }

    const independent& GetSister() const { return m_sister; }
    void SetSister(const independent& sister) { m_sister = sister; }

  private:
    const independent& m_sister;
}

消除依赖关系

dependent的实例可能需要一个independent对象来工作,但是不一定非得有一个。可以定义一个成员指针指向另一个对象。

class dependent
{
  public:
    dependent()
    {
      // Initialize stuff
    }

    const independent* GetSister() const { return m_sister; }
    void SetSister(const independent* sister) { m_sister = sister; }

  private:
    const independent* m_sister;
}

初始化顺序

现在让我们确保在依赖对象准备好时,independent 对象也已经准备就绪。

初始化函数

您可以使用指针和一个 Init() 函数。

Independent* sister = NULL;
Dependent* brother = NULL;

Init()
{
    assert(brother == NULL && sister == NULL);
    sister = new independent();
    brother = new dependent(sister);
}

按需创建

您可以在函数中使用静态变量来强制创建的顺序。这样做虽然可行,但如果需要多个类的实例就会变得很麻烦。

independent& GetSister()
{
    static independent sister; // Initialized on first use
    return sister;
}

dependent& GetBrother()
{
    static dependent brother(GetSister()); // Initialized on first use
    return brother;
}

避免使用全局变量

最后,最干净的方法可能是尽量摆脱全局变量。相反,你应该使用本地变量(你完全掌控它们的创建顺序)。当然,你需要将它们传递给需要它们的函数,如果有很多这样的函数,那么这会变得繁琐。

但是,当你确定所有需要这些对象的代码时,将其移动到一个对象中可能是有意义的,你最初的两个全局变量只需成为该对象的成员即可。

class Foo
{
  public:
    Foo(): m_sister(...), m_brother(...)
    {
       // Initialize stuff
    }

    // Functions using the objects go here

  private:
    independent m_sister;
    dependent m_brother;
}

结论

嗯,我意识到我的回答有点长了,有点过头了。这是重点:

  • 尽可能避免使用全局变量。
  • 表达类之间的依赖关系。
  • 如果封装代码和状态变量能够使事情变得更加容易,请不要犹豫,将其封装到一个类中,这最终是有意义的。

8

这实际上是双向的。一方面,它确保“代码不会对你撒谎”。另一方面,它有意泄漏实现细节。对于一个层次,比如CreditCardCreditCardProcessor,情况并不那么糟糕。但是当您开始构建分层软件时,您很快就会开始暴露不需要暴露的状态。举个具体例子,在我工作的某个应用程序中,我将“构造函数中的参数”模式转换为单例,并从应用程序源代码中删除了7kB。这从我们的代码中删除了很多“噪音”。 - Cort Ammon
1
@CortAmmon:如果说我从使用C++中学到了什么,那就是它明确的目标之一是减少代码库中的代码量。这不是你在C++中追求的东西。即使像auto这样的新特性也不是为了节省打字而存在的——它们的存在是为了使代码更加通用。像auto这样的特性有好有坏,但都可以减少某种噪音。噪音不是你想要衡量的东西——你想要通过失败概率来衡量。而失败概率并不是信噪比的函数,相反,它是反过来的。 - user541686
@Mehrdad:我想每个人都为不同的公司开发,结果也不同。在某些公司中,噪音实际上是失败概率的主要原因之一。噪音会迫使你要么理解代码,要么猜测什么是噪音和什么是信号。根据我的经验(与一组公司相关),很容易让程序变得足够大,以至于几乎无法理解,所以人们经常猜测,并且猜错了。在其他情况下,我想这可能不那么正确。 - Cort Ammon
作为我观点的病态例子,我认为add(resultFactory, securityCtx, mathCtx, varA, varASecurityCtx, varAAtomicity, varB, varBSecurityCtx, varBAtomicity)的特定性不值得以可读性为代价,相比其全局使用的表亲add(varA, varB),尤其是对于那些实际需要所有这些非全局上下文的人来说,完整版本是可用的。 - Cort Ammon
@Cort:我明白你的意思...不过我认为你可以通过将所有“上下文”参数打包成一个单独的对象来缓解这个问题。 - user541686

6
如果在编译单元内两个全局变量是有序的,则它们将按顺序初始化。如果它们在不同的编译单元中以不同的顺序出现(例如在不同的.h文件中,并由不同的.cpp以不同的顺序包含),则结果未定义。
如果您必须按顺序使用它们,但无法将它们放在同一个编译单元中,请考虑使用单例模式。
class Independent
{
    public:
        static Independent& getO1()
        {
            static Independent o1;
            return o1;
        }
};

class Dependent
{
    public:
        static Dependent& getO2()
        {
            Independent::getO1(); // force o1 to exist first
            static Dependent o2;
            return o2;
        }
};

这里使用了静态局部变量初始化规则,确保了无论如何包含 .h 文件,都可以按照顺序进行初始化。


1
我认为单例在这里有些过度设计了:一个简单的函数返回一个静态实例就可以了。 - juanchopanza
@juanchopanza:单例模式实际上是一个不好的解决方案,因为问题没有提到对象的唯一性。 - Julien Guertault
@JulienGuertault 正是如此。它会对这些类型施加不必要的限制。但归根结底,真正的问题在于,应该尽可能避免使用全局变量,无论它们是单例还是其他类型。 - juanchopanza
@juanchopanza:如果他只是将函数移出类并没有使用“singleton”这个词,那么它就和其他大多数答案一样了。再看一遍,这段代码实际上并不是一个单例。它们只是全局变量,使用“singleton”模式来避免问题。 - Mooing Duck
@JulienGuertault 这个答案没有强制要求对象的唯一性。它与您的“按需创建”模式完全相同。 - Mooing Duck
@MooingDuck:从技术上讲,它并没有,但原则上单例模式的概念确实存在。特别是在这个答案中,将函数放在类内部会对对象的定义施加限制。它在说:“使用这些静态函数并且只有一个实例,否则就自己去实现”。 - Julien Guertault

2

使用静态函数局部变量代替全局变量。文件中的全局变量总是在调用任何函数之前初始化,因此可以使用非内联函数调用来强制全局变量按顺序进行初始化。这样做的附加好处是,您可以自由而安全地添加更多相互依赖的全局变量,而它们就像神奇般地工作一样。

//header
class independent; 
independent& o1();
class dependent;
dependent& o2();

//cpp1 
independent& o1() {
   static independent o;
   return o;
}

//cpp2
dependent& o2() {
   o1(); //affirm that o1 is constructed first.
   static dependento; //now construct o2
   return o;
}

int main() {
    o1().thingy = -1;
    o2().thingy = 3;
}

但它们实际上不需要在任何cpp文件中定义。我不确定你在这里的意思。如果你在每个翻译单元中包含显式特化的定义,它们会违反ODR(One Definition Rule)。否则,你需要使用extern并在一个特定的翻译单元中放置一个定义。模板在这里如何帮助? - dyp
@DyP:问题中的代码在MSVC中可以工作。我不记得为什么它能够工作,但我认为这与模板静态全局变量被允许违反ODR有关,因为没有TU可能知道在另一个TU中已经特化了哪些模板,因此它为每个TU创建一个模板实例(模板函数实例也是如此),然后链接器将它们合并。也有可能我展示的技巧是错误的,而问题中的代码只在MSVC中工作。但我非常确定我记得类似于这样的东西是标准的。 - Mooing Duck
@DyP: § 14.6.4.1/7 "类模板的静态数据成员的特化可以在一个翻译单元中具有多个实例化点,并且除了上述描述的实例化点之外,对于任何在翻译单元中具有实例化点的这样的特化,翻译单元的结束也被视为实例化点。类模板的特化在翻译单元内最多有一个实例化点。任何模板的特化都可以在多个翻译单元中具有实例化点。" - Mooing Duck
抱歉,我对显式特化的含义感到困惑。看起来像是定义,但是[temp.expl.spec]/13说,“如果声明包括初始化程序,则模板静态数据成员的显式特化是定义; 否则,它是一个声明。”(这意味着您不需要使用extern,但原始注释仍应有效。) - dyp
哦,这意味着定义完全丢失了 :/ (clang++ 也报告了链接器错误,未定义的引用) - dyp
显示剩余4条评论

1
如果你有n个全局变量,其中n>0,那么构造它们的逻辑位置应该在包含main()的程序单元中。按照所需顺序在文件中实例化它们。

0

它们的构造顺序无法保证。

消除这种依赖是最好的选择,尽量避免使用全局变量。但如果必须使用,并且希望确保一个在另一个之前运行,最好使用指针和返回指针的函数。

independent * pindependent = null;
dependent * pdependent = null;

independent * getIndependent()
{
  if(pindependent == null)
    pindependent = new independent(); // fixed the bug pointed to by the Mooing Duck
  return pindependent;
}

dependent * getDependent()
{
  if(pdependent == null)
  {
    getIndependent();
    pdependent = new dependent(); // fixed the bug pointed to by the Mooing Duck
  }

  return pdependent;
}

在头文件中,您可以公开函数(可选择使用extern):
extern independent * getIndependent();
extern dependent * getDependent();

@Mooing Duck:1. 这段代码展示了原则 - 它并不是为了直接运行而设计的。2. 动态实例化并不是坏事 - 实际上它经常很好用 - 它将初始化延迟到需要的时候,有时甚至永远不会发生。3. 我认为你的评论没有建设性,而且很冒犯,所以请停止叫唤,支持你的话语。 - Sergey
我的评论不见了,所以我不记得我说了什么,但是从你的回复中我可以看出我的帖子很冒犯,所以我谦虚地道歉,不管我当时说了什么。我可能对这页上我不同意的答案感到沮丧,可能措辞太强烈了。 - Mooing Duck
1
话虽如此,你应该修复这个缺陷,使其在每次调用这些函数时不会分配新对象并消除内存泄漏。我仍然不同意使用动态内存来延迟构造直到第一次使用,在这种情况下有更简单的方法可以实现。 - Mooing Duck
好了,现在好多了。但是你知道我对Stack Overflow的看法吗?这里有很多刻薄的人,只要别人的答案有一个或两个缺陷,或者反映了不同的观点,就会跳出来批评和投票。我真的不喜欢这样——我不认为花半个小时来完善一个非常简单的代码是一件有趣的事情。如果有人想让程序员为他们编写程序,他们应该雇用开发人员,在这里我们应该提供一个粗略的解决方案、一个想法,而不是一个完整的工作实现。感谢您指出错误。 - Sergey
@Sergey 我同意你的观点,但也要注意我们是人类,而不是一种类似于瓦肯程序员的存在 :) - Manu343726

0
使用全局指针和函数调用来保证正确的顺序:
//(!!PSEUDO-CODE!!)

Independent* o1;
Dependent* o2;

void CreateGlobalData();
void DestroyGlobalData();
//...    
void CreateGlobalData() {
    o1 = new Independent;
    o2 = new Dependent(*o1); //This is an assumption that the
                             //Dependent type takes an Independent parameter.
}
//...
void DestroyGlobalData() {
    delete o2;  //Always destroy in reverse order!
    delete o1;
}

有更简单的方法。 - Mooing Duck
@MooingDuck 一个函数调用怎么可能不容易呢? - Casey
怎么样,不使用初始化函数调用、去初始化、指针和new呢?这种方式也需要一个函数了解所有可能的全局变量。 - Mooing Duck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接