Почему порядок уничтожения этих локальных статических объектов НЕ является обратным порядку их инициализации?

У меня есть два локальных статических объекта, один и два. Конструктор и деструктор One получают доступ к Two через GetTwo():

#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++ добавил некоторые причудливые гарантии для локальных статических переменных, таких как потокобезопасность. Если не undefined, то является ли такое поведение частью этих гарантий? (Что было бы неплохо, так как это помешало бы вам выстрелить себе в ногу, когда деструктор One использует разрушенный экземпляр Two.) И что гарантируется, если GetOne и GetTwo находятся в разных единицах перевода?

РЕДАКТИРОВАТЬ:

Спасибо за комментарии, теперь я вижу, что объект считается построенным только после возврата его конструктора, а не при первом входе в него, поэтому Two фактически создается до One.

Также я попытался прочитать стандарт и нашел это в стандарте С++ 11, раздел 6.7, пункт 4:

Нулевая инициализация (8.5) всех переменных области блока со статической продолжительностью хранения (3.7.1) или длительностью хранения потока (3.7.2) выполняется до того, как произойдет любая другая инициализация. Константная инициализация (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 после их уничтожения, поэтому поведение undefined. Но, по крайней мере, это детерминировано.


person bberg    schedule 16.07.2015    source источник
comment
Обратите внимание, что вы вызываете GetTwo до того, как статический экземпляр One будет полностью создан.   -  person Captain Obvlious    schedule 16.07.2015
comment
@CaptainObvlious, в этом нет ничего плохого   -  person M.M    schedule 16.07.2015
comment
@MattMcNabb Никогда не говорил, что есть.   -  person Captain Obvlious    schedule 16.07.2015
comment
@Captain Obvlious Спасибо, кажется, я понял. Я предполагаю, что стандарт указывает, что объекты должны быть полностью созданы в обратном порядке их уничтожения. Мы начинаем строительство Один раньше Два, но заканчиваем строительство Два раньше Один. Таким образом, Двойка считается построенной раньше Единицы.   -  person bberg    schedule 16.07.2015


Ответы (2)


Фактический стандартный текст в С++ 14 [basic.start.term]:

Если завершение конструктора или динамическая инициализация объекта со статической продолжительностью хранения следуют до завершения другого, завершение деструктора второго упорядочено до инициации деструктора первого. [Примечание: это определение допускает одновременное уничтожение. -конец примечания]

В вашем коде two создается во время конструктора one. Следовательно, завершение конструктора two выполняется до завершения конструктора one.

Таким образом, завершение деструктора one происходит до завершения деструктора two, что объясняет то, что вы видите.

person M.M    schedule 16.07.2015

Измените свой ctor на:

  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;
  }

теперь вы увидите, что Two заканчивает строительство раньше, чем One. Таким образом, Two регистрируется для уничтожения до того, как это сделает One, и уничтожается после, потому что он был фактически построен (полностью) первым.

Start One construct
Two construct
twoval is: It's two!
Finish One construct
One destruct
twoval is: It's two!
Two destruct
person Yakk - Adam Nevraumont    schedule 16.07.2015