2

Этот пост может показаться слишком длинным для краткого вопроса в конце его. Но мне также нужно описать шаблон дизайна, который я только что придумал. Может быть, это обычно используется, но я никогда не видел его (или, может быть, он просто не работает :).Упорядоченная статическая инициализация потокобезопасных классов

Во-первых, вот код, который (насколько мне известно) имеет неопределенное поведение из-за «статического инициализационного порядка фиаско». Проблема заключается в том, что инициализация испанского :: s_englishToSpanish зависит от английского :: s_numberToStr, которые являются как статические, так и инициализируется в разных файлах, так что порядок этих инициализаций не определено:

Файл: English.h

#pragma once 

#include <vector> 
#include <string> 

using namespace std; 

struct English { 
    static vector<string>* s_numberToStr; 
    string m_str; 

    explicit English(int number) 
    { 
     m_str = (*s_numberToStr)[number]; 
    } 
}; 

Файл: English.cpp

#include "English.h" 

vector<string>* English::s_numberToStr = new vector<string>(/*split*/ 
[]() -> vector<string> 
{ 
    vector<string> numberToStr; 
    numberToStr.push_back("zero"); 
    numberToStr.push_back("one"); 
    numberToStr.push_back("two"); 
    return numberToStr; 
}()); 

Файл: Spanish.h

#pragma once 

#include <map> 
#include <string> 

#include "English.h" 

using namespace std; 

typedef map<string, string> MapType; 

struct Spanish { 
    static MapType* s_englishToSpanish; 
    string m_str; 

    explicit Spanish(const English& english) 
    { 
     m_str = (*s_englishToSpanish)[english.m_str]; 
    } 
}; 

Файл: Spanish.cpp

#include "Spanish.h" 

MapType* Spanish::s_englishToSpanish = new MapType(/*split*/ 
[]() -> MapType 
{ 
    MapType englishToSpanish; 
    englishToSpanish[ English(0).m_str ] = "cero"; 
    englishToSpanish[ English(1).m_str ] = "uno"; 
    englishToSpanish[ English(2).m_str ] = "dos"; 
    return englishToSpanish; 
}()); 

Файл: StaticFiasco.h

#include <stdio.h> 
#include <tchar.h> 
#include <conio.h> 

#include "Spanish.h" 

int _tmain(int argc, _TCHAR* argv[]) 
{ 
    _cprintf(Spanish(English(1)).m_str.c_str()); // may print "uno" or crash 

    _getch(); 
    return 0; 
} 

Чтобы решить проблему статического порядка инициализации, мы используем конструкт-на-первого использования идиомы, и сделать эти статические инициализацию функция локального, так как:

Файл: English.h

#pragma once 

#include <vector> 
#include <string> 

using namespace std; 

struct English { 
    string m_str; 

    explicit English(int number) 
    { 
     static vector<string>* numberToStr = new vector<string>(/*split*/ 
     []() -> vector<string> 
     { 
      vector<string> numberToStr_; 
      numberToStr_.push_back("zero"); 
      numberToStr_.push_back("one"); 
      numberToStr_.push_back("two"); 
      return numberToStr_; 
     }()); 

     m_str = (*numberToStr)[number]; 
    } 
}; 

Файл: Spanish.h

#pragma once 

#include <map> 
#include <string> 

#include "English.h" 

using namespace std; 

struct Spanish { 
    string m_str; 

    explicit Spanish(const English& english) 
    { 
     typedef map<string, string> MapT; 

     static MapT* englishToSpanish = new MapT(/*split*/ 
     []() -> MapT 
     { 
      MapT englishToSpanish_; 
      englishToSpanish_[ English(0).m_str ] = "cero"; 
      englishToSpanish_[ English(1).m_str ] = "uno"; 
      englishToSpanish_[ English(2).m_str ] = "dos"; 
      return englishToSpanish_; 
     }()); 

     m_str = (*englishToSpanish)[english.m_str]; 
    } 
}; 

Но теперь у нас есть еще одна проблема. Из-за локальных статических данных функции ни один из этих классов не является потокобезопасным. Чтобы решить эту проблему, мы добавляем к обоим классам статическую переменную-член и функцию инициализации для нее. Затем внутри этой функции мы принудительно инициализируем все локально-локальные статистические данные, вызывая одну функцию, которая имеет локально-локальные статические данные. Таким образом, эффективно мы инициализируем все в начале программы, но все же контролируем порядок инициализации. Так что теперь наши классы должны быть поточно-:

Файл: English.h

#pragma once 

#include <vector> 
#include <string> 

using namespace std; 

struct English { 
    static bool s_areStaticsInitialized; 
    string m_str; 

    explicit English(int number) 
    { 
     static vector<string>* numberToStr = new vector<string>(/*split*/ 
     []() -> vector<string> 
     { 
      vector<string> numberToStr_; 
      numberToStr_.push_back("zero"); 
      numberToStr_.push_back("one"); 
      numberToStr_.push_back("two"); 
      return numberToStr_; 
     }()); 

     m_str = (*numberToStr)[number]; 
    } 

    static bool initializeStatics() 
    { 
     // Call every member function that has local static data in it: 
     English english(0); // Could the compiler ignore this line? 
     return true; 
    } 
}; 
bool English::s_areStaticsInitialized = initializeStatics(); 

Файл: Spanish.h

#pragma once 

#include <map> 
#include <string> 

#include "English.h" 

using namespace std; 

struct Spanish { 
    static bool s_areStaticsInitialized; 
    string m_str; 

    explicit Spanish(const English& english) 
    { 
     typedef map<string, string> MapT; 

     static MapT* englishToSpanish = new MapT(/*split*/ 
     []() -> MapT 
     { 
      MapT englishToSpanish_; 
      englishToSpanish_[ English(0).m_str ] = "cero"; 
      englishToSpanish_[ English(1).m_str ] = "uno"; 
      englishToSpanish_[ English(2).m_str ] = "dos"; 
      return englishToSpanish_; 
     }()); 

     m_str = (*englishToSpanish)[english.m_str]; 
    } 

    static bool initializeStatics() 
    { 
     // Call every member function that has local static data in it: 
     Spanish spanish(English(0)); // Could the compiler ignore this line? 
     return true; 
    } 
}; 

bool Spanish::s_areStaticsInitialized = initializeStatics(); 

И вот вопрос: Возможно ли, что некоторые компилятор может оптимизировать эти вызовы функций (конструкторы в этом случае), которые имеют локальные статические данные? Таким образом, вопрос заключается в том, что в точности означает «наличие побочных эффектов», что, на мой взгляд, означает, что компилятору не разрешено его оптимизировать. Имеет ли локально локальные статические данные достаточно, чтобы заставить компилятор думать, что вызов функции нельзя игнорировать?

+2

Если у вас уже есть C++ 11 (как указано с помощью lambdas), почему бы не использовать инициализацию списка для векторов сразу? –

+2

1) Компилятор Intel C++ еще не поддерживает списки инициализаторов. 2) Если мне нужно использовать функцию или загрузить из файла для инициализации данных, список инициализаторов не будет применяться. 3) Более того, список инициализаторов ничего не помог бы решить проблему статического инициализации, о которой я говорю, поэтому я не вижу, насколько они актуальны. – zeroes00

ответ

1

Раздел 1.9 «Выполнение программы» [intro.execution] из стандарта C++ 11 говорит, что

1 семантические описания в настоящем стандарте определить параметризированную недетерминированную абстрактную машину. ... соответствующие реализации необходимы для подражания (только) наблюдаемого поведения абстрактной машины, как объяснено ниже.
...

5 Соответствующая реализация, выполняющая хорошо сформированную программу, должна обеспечивать такое же наблюдаемое поведение, как и одно из возможных исполнений соответствующего экземпляра абстрактной машины с той же программой и тем же входом.
...

Наималейшие требований к соответствующей реализации являются:
- Доступ к изменчивым объектам оцениваются строго в соответствии с правилами абстрактной машины.
- При завершении программы все данные, записанные в файлы, должны быть идентичны одному из возможных результатов выполнения программы в соответствии с абстрактной семантикой.
- Динамика ввода и вывода интерактивных устройств должна происходить таким образом, чтобы запрос вывода фактически доставлялся до того, как программа ждет ввода. Что представляет собой интерактивное устройство, оно определяется реализацией.
Все это называется наблюдаемым поведением программы.
...

12 Доступ к объекту, назначенный летучего glvalue (3.10), изменение объекта, вызов функции ввода/вывода библиотеки, или вызов функции, которая делает любой из этих операций являются все побочные эффекты, которые являются изменения состояние среды исполнения.

Кроме того, в 3.7.2 «Автоматическая продолжительность хранения» [basic.stc.auto] сказано, что

3 Если переменная с автоматической продолжительности хранения имеет инициализацию или деструктор с побочными эффектами , он не должен быть уничтожен до конца его блока и не будет устранен как оптимизация, даже если он кажется неиспользованным, за исключением того, что объект класса или его копирование/перемещение могут быть устранены, как указано в 12.8.

12.8-31 описывает копирование, которое, по моему мнению, здесь не имеет значения.

Итак, вопрос в том, имеют ли инициализацию локальные переменные побочные эффекты, которые не позволяют оптимизировать его. Поскольку он может выполнять инициализацию статической переменной с адресом динамического объекта, я думаю, что он создает достаточные побочные эффекты (например, модифицирует объект). Кроме того, вы можете добавить там операцию с изменчивым объектом, тем самым вводя наблюдаемое поведение, которое невозможно устранить.

1

Хорошо, в двух словах:

  1. Я не могу понять, почему статические члены класса должны быть открытыми - они деталь реализации.

  2. Не делайте их приватными, но вместо этого делайте их членами модуля компиляции (где код, реализующий ваши классы, будет).

  3. Для выполнения статической инициализации используйте boost::call_once.

Инициализация при первом использовании относительно легко обеспечить соблюдение порядка, это гораздо труднее выполнить в целях уничтожения. Обратите внимание, однако, что функция, используемая в call_once, не должна генерировать исключение. Поэтому, если он может завершиться неудачей, вы должны покинуть какое-то несостоявшееся состояние и проверить это после вызова.

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

+0

1) Все публично в моем глупом примере, потому что я хотел держать его как можно короче. Добавление public: и private: добавляет ненужный беспорядок для того, что должно быть просто примером. 2) Не будут ли данные по-прежнему восприимчивыми к статическому фиаско порядка инициализации, даже если они полностью находятся в исходном файле (cpp)? 3) Я никогда не слышал о boost :: call_once. Спасибо, что указали это, но, глядя на источник, он добавит некоторые издержки во время выполнения и, вероятно, сделает компилятор неспособным встроить функцию, которая должна вызвать этот boost :: call_once. Я не могу позволить себе накладные расходы. – zeroes00

+0

С вызовом call_once вы начинаете с указателя NULL, а не с объекта, а один раз флаг, который инициализируется CALL_INIT (я думаю). У вас есть функция «создать», которую вы знаете, нужно вызывать ровно один раз, и вы вызываете ее, после чего вы знаете, что ваш объект создан. Если вам нужно что-то еще, чтобы создать сначала, то получите функцию «создать», чтобы использовать другой объект, который должен быть уже создан. Конечно, у вас будут большие проблемы, если у вас есть круглые ссылки. – CashCow

0

Возможно, вам нужно выполнить дополнительную работу для управления порядком инициализации. как,

class staticObjects 
{ 
    private: 
    vector<string>* English::s_numberToStr; 
    MapType* s_englishToSpanish; 
}; 

static staticObjects objects = new staticObjects(); 

, а затем определить некоторые интерфейсы для его получения.

1

Почему бы вам просто не спрятать английский :: s_numberToStr за публичной статической функцией и полностью пропустить синтаксис конструктора? Используйте DCLP для обеспечения безопасности потока.

Я настоятельно рекомендую избегать статических переменных класса, инициализация которых включает в себя нетривиальные побочные эффекты. Как общая схема проектирования, они, как правило, вызывают больше проблем, чем они решают. Какие бы проблемы с производительностью вы ни испытывали, здесь нужно обоснование, потому что я сомневаюсь, что они поддаются измерению в реальных условиях.

Смежные вопросы