为什么这些函数本地静态对象的销毁顺序不是它们初始化顺序的相反顺序?

7

我有两个函数局部的静态对象,One和Two。One的构造函数和析构函数都通过GetTwo()访问Two:

#include <iostream>

struct One;
struct Two;

const One& GetOne();
const Two& GetTwo();

struct Two {
  const char* value = "It's two!";
  Two() { std::cout << "Two construct" << std::endl; }
  ~Two() { std::cout << "Two destruct" << std::endl; }
};

struct One {
  One() {
    std::cout << "One construct" << std::endl;
    const char* twoval = GetTwo().value;
    std::cout << "twoval is: " << twoval << std::endl;
  }
  ~One() {
    std::cout << "One destruct" << std::endl;
    const char* twoval = GetTwo().value;
    std::cout << "twoval is: " << twoval << std::endl;
  }
};

const One& GetOne() {
  static One one;
  return one;
}

const Two& GetTwo() {
  static Two two;
  return two;
}

int main(void) {
  GetOne();
}

我用g++ 4.8.4编译这个文件: g++ -std=c++11 [文件名]
然后它会输出:
One construct
Two construct
twoval is: It's two!
One destruct
twoval is: It's two!
Two destruct

它们是按相同的顺序构建和析构的!我读到对于同一翻译单元中的C++类的静态变量,其销毁顺序始终是构造顺序的倒序。但我想不是吧?或者说这是未定义的行为吗?
此外,我听说,在C++11中,C++委员会为函数局部静态变量添加了一些花哨的保证,如线程安全。如果不是未定义的话,那么这个行为是否是这些保证的一部分呢?(这将非常好,因为它可以防止您使用已销毁的Two实例来使用One的析构函数自杀。)如果GetOne和GetTwo在不同的翻译单元中,那么什么是保证的呢?
编辑: 感谢迄今为止的评论,我现在明白了一个对象只有在其构造函数返回时才被认为是构造完成的,而不是在首次进入时,所以Two实际上是在One之前构造的。
另外,我尝试阅读标准并在C++11标准的第6.7节第4项中找到了以下内容:
“所有具有静态存储期(3.7.1)或线程存储期(3.7.2)的块作用域变量的零初始化(8.5)在其他任何初始化之前都要执行。如果适用,块作用域实体的常量初始化(3.6.2)将在其首次进入块之前执行。......这样一个变量在控制流第一次通过其声明时被初始化;该变量在完成其初始化时被认为已初始化。”
而对于销毁,6.7将我们指向3.6.3,其中写道:
“如果具有静态存储期的对象的构造函数或动态初始化完成按顺序排列在另一个对象的构造函数或动态初始化完成之前,则第二个对象的析构函数的完成在第一个对象的析构函数的启动之前进行排序。”
因此,如果我理解正确:对于函数局部静态对象,它们的构造是在运行时"按序列化"的,基于函数调用的顺序。而且,无论它们定义在哪个翻译单元中,它们都将以相反于该运行时依赖顺序的顺序进行销毁。
这听起来正确吗?这将使这成为一个不错的静态顺序初始化解决方案。话虽如此,我认为您仍然可以使用以下代码自杀:
#include <iostream>

struct One;
struct Two;

const One& GetOne();
const Two& GetTwo();
void PrintOneValue(const One& one);

struct Two {
  Two() { std::cout << "Two construct" << std::endl; }
  ~Two() {
    std::cout << "start Two destruct" << std::endl;
    PrintOneValue(GetOne());
    std::cout << "end Two destruct" << std::endl;
  }
};

struct One {
  const char* value = "It's one!";
  One() {
    std::cout << "start One construct" << std::endl;
    GetTwo();
    std::cout << "end One construct" << std::endl;
  }
  ~One() {
    std::cout << "One destruct" << std::endl;
  }
};

void PrintOneValue(const One& one) {
  std::cout << "One's value is: " << one.value << std::endl;
}

const One& GetOne() {
  static One one;
  return one;
}

const Two& GetTwo() {
  static Two two;
  return two;
}

int main(void) {
  GetOne();
}

这将输出:

start One construct
Two construct
end One construct
One destruct
start Two destruct
One's value is: It's one!
end Two destruct

在数据被销毁后,它仍然可以访问One的数据,这会导致未定义的行为。但至少它是可确定的。


4
请注意,在静态实例化One完全构造之前您正在调用GetTwo - Captain Obvlious
@CaptainObvlious,这没有任何问题。 - M.M
2
@MattMcNabb 从来没有说过有这样的事情。 - Captain Obvlious
@Captain Obvlious 谢谢,我想我明白了。我猜标准规定对象必须按照它们销毁的相反顺序_完全构造_。我们在 One 之前开始构造,但在 One 之前完成 Two 的构造。因此,在 One 之前,Two 被认为已经“被构造”。 - user3217070
2个回答

6
实际上,C++14中的标准文本[basic.start.term]是这样的:
如果具有静态存储期的对象的构造函数或动态初始化在另一个对象之前被序列化完成,则第二个对象的析构函数的完成将被序列化在第一个对象的析构函数的初始化之前。[注意:此定义允许并发销毁。-注]
在您的代码中,`two`是在`one`的构造函数期间构建的。因此,`two`的构造函数的完成被序列化在`one`的构造函数完成之前。
因此,`one`的析构函数的完成被序列化在`two`的析构函数完成之前,这解释了您所看到的现象。

4

将您的构造函数更改为:

  One() {
    std::cout << "Start One construct" << std::endl;
    const char* twoval = GetTwo().value;
    std::cout << "twoval is: " << twoval << std::endl;
    std::cout << "Finish One construct" << std::endl;
  }

现在你将看到TwoOne之前完成构建。因此Two被注册为在One之前被销毁,并且因为它实际上是首先完全构建的,所以在One之后被销毁。

Start One construct
Two construct
twoval is: It's two!
Finish One construct
One destruct
twoval is: It's two!
Two destruct

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