Порядок инициализации статических переменных
От: Посторонним В. Беларусь  
Дата: 11.12.13 10:28
Оценка:
Есть такой код

a.h
...
namespace X
{
    const std::string Foo = "foo";
    inline std::string getFoo()
    {
        return Foo;
    }
}
...


a.cpp:
#include "a.h"
...
namespace X
{
   const string Default_Foo = getFoo();
}
...


Само-собой в проекте есть еще файлы, который инклудят a.h

На старте программа валится в segfault в выделенной строчке. Исследование показало, что:

1. в программе создается несколько копий Foo
-bash-4.2# nm -oC a.out | grep Foo
/usr/local/bin/RESEPT/www/authmod.fcgi:3c162c50 b X::Foo
/usr/local/bin/RESEPT/www/authmod.fcgi:3c162990 b X::Foo
/usr/local/bin/RESEPT/www/authmod.fcgi:3c1641b0 b X::Foo


2. В процессе инициализации Default_Foo вызывается getFoo(), которая берет не уже инициализированую Foo из единицы компиляции a.cpp (как я бы ожидал), а Foo из другой единицы компиляции, в которой, волею судеб, Foo еще не инициализирована.
Что приводит в segfault-у.

Кто-нибудь может объяснить, верно ли такое поведение?
Как лучше всего избегать такого рода ошибок?
Re: Порядок инициализации статических переменных
От: HolyNick  
Дата: 11.12.13 10:45
Оценка:
Здравствуйте, Посторонним В., Вы писали:

ПВ>Есть такой код


ПВ>a.h

ПВ>
ПВ>...
ПВ>namespace X
ПВ>{
ПВ>    const std::string Foo = "foo";
ПВ>    inline std::string getFoo()
ПВ>    {
ПВ>        return Foo;
ПВ>    }
ПВ>}
ПВ>...
ПВ>


ПВ>a.cpp:

ПВ>
ПВ>#include "a.h"
ПВ>...
ПВ>namespace X
ПВ>{
ПВ>   const string Default_Foo = getFoo();
ПВ>}
ПВ>...
ПВ>


ПВ>Само-собой в проекте есть еще файлы, который инклудят a.h


ПВ>На старте программа валится в segfault в выделенной строчке. Исследование показало, что:


ПВ>1. в программе создается несколько копий Foo

ПВ>
ПВ>-bash-4.2# nm -oC a.out | grep Foo
ПВ>/usr/local/bin/RESEPT/www/authmod.fcgi:3c162c50 b X::Foo
ПВ>/usr/local/bin/RESEPT/www/authmod.fcgi:3c162990 b X::Foo
ПВ>/usr/local/bin/RESEPT/www/authmod.fcgi:3c1641b0 b X::Foo
ПВ>


ПВ>2. В процессе инициализации Default_Foo вызывается getFoo(), которая берет не уже инициализированую Foo из единицы компиляции a.cpp (как я бы ожидал), а Foo из другой единицы компиляции, в которой, волею судеб, Foo еще не инициализирована.

ПВ>Что приводит в segfault-у.

ПВ>Кто-нибудь может объяснить, верно ли такое поведение?

ПВ>Как лучше всего избегать такого рода ошибок?

Глобальные\статические переменные определенные в одной единице трансляции скорее всего будут создаваться\инициализироваться в порядке их определения. Порядок инициализации
глобальных\статических переменных в разных единицах трансляции определяется порядком их(сpp файлов) обработки линковщиком, те для программиста заранее неизвестен в общем-то.

Лучше избегать связи между глобальными переменными, да и их самих желательно иметь поменьше.
//в cpp файле, а не h
std::string getDefaultFoo()
{
 static const std::string Foo = "foo";
 return Foo;
}
Re: Порядок инициализации статических переменных
От: Kernan Ниоткуда https://rsdn.ru/forum/flame.politics/
Дата: 11.12.13 10:46
Оценка: -1
Здравствуйте, Посторонним В., Вы писали:

ПВ>Есть такой код

Инициализация стат. переменных это UB и зависит от компилятора и фазы луны. Именно поэтому многие люди делают вызов метод/функцию initialize и вызывают её в самом начале main.
Sic luceat lux!
Re[2]: Порядок инициализации статических переменных
От: HolyNick  
Дата: 11.12.13 10:50
Оценка:
Здравствуйте, HolyNick, Вы писали:

HN>Здравствуйте, Посторонним В., Вы писали:


ПВ>>Есть такой код


ПВ>>a.h

ПВ>>
ПВ>>...
ПВ>>namespace X
ПВ>>{
ПВ>>    const std::string Foo = "foo";
ПВ>>    inline std::string getFoo()
ПВ>>    {
ПВ>>        return Foo;
ПВ>>    }
ПВ>>}
ПВ>>...
ПВ>>


ПВ>>a.cpp:

ПВ>>
ПВ>>#include "a.h"
ПВ>>...
ПВ>>namespace X
ПВ>>{
ПВ>>   const string Default_Foo = getFoo();
ПВ>>}
ПВ>>...
ПВ>>


ПВ>>Само-собой в проекте есть еще файлы, который инклудят a.h


ПВ>>На старте программа валится в segfault в выделенной строчке. Исследование показало, что:


ПВ>>1. в программе создается несколько копий Foo

ПВ>>
ПВ>>-bash-4.2# nm -oC a.out | grep Foo
ПВ>>/usr/local/bin/RESEPT/www/authmod.fcgi:3c162c50 b X::Foo
ПВ>>/usr/local/bin/RESEPT/www/authmod.fcgi:3c162990 b X::Foo
ПВ>>/usr/local/bin/RESEPT/www/authmod.fcgi:3c1641b0 b X::Foo
ПВ>>


ПВ>>2. В процессе инициализации Default_Foo вызывается getFoo(), которая берет не уже инициализированую Foo из единицы компиляции a.cpp (как я бы ожидал), а Foo из другой единицы компиляции, в которой, волею судеб, Foo еще не инициализирована.

ПВ>>Что приводит в segfault-у.

ПВ>>Кто-нибудь может объяснить, верно ли такое поведение?

ПВ>>Как лучше всего избегать такого рода ошибок?

HN>Глобальные\статические переменные определенные в одной единице трансляции скорее всего будут создаваться\инициализироваться в порядке их определения. Порядок инициализации

HN>глобальных\статических переменных в разных единицах трансляции определяется порядком их(сpp файлов) обработки линковщиком, те для программиста заранее неизвестен в общем-то.

HN>Лучше избегать связи между глобальными переменными, да и их самих желательно иметь поменьше.

HN>
HN>//в cpp файле, а не h
HN>std::string getDefaultFoo()
HN>{
HN> static const std::string Foo = "foo";
HN> return Foo;
HN>}
HN>


PS: Обработка линковщиком конечно же объектных файлов.
Re: Порядок инициализации статических переменных
От: watchmaker  
Дата: 11.12.13 11:24
Оценка: 37 (2)
Здравствуйте, Посторонним В., Вы писали:


ПВ>Кто-нибудь может объяснить, верно ли такое поведение?

Вполне верно. Тут нарушение ODR. Функция getFoo() имеет external linkage, но компилируется в каждой единице трансляции (из-за inline). В то же время переменная Foo имеет internal linkage (из-за const) и в каждой единице трансляции есть своя копия. В результате получается, что при инициализации Default_Foo вызывается getFoo, но не обязательно из той же единицы трансляции. А в другой единице getFoo уже работает с другой переменной Foo, которая ещё не инициализированна. То есть не смотря на то, что на первый взгляд текст функции getFoo везде одинаков, в результате компиляции появляется целое семейство различных функций getFoo. При сборке из них выбирается случайная — и вот результат.

Тут выше в теме уже ответили, что порядок инициализации глобальных переменных из разных единиц трансляции не определён. Это, конечно, тоже играет свою роль в твоей проблеме. Но главная причина — нарушении ODR.

ПВ>Как лучше всего избегать такого рода ошибок?

Можно просто избавится от множества копий Foo. Во-первых, сомнительно, что тебе вообще нужные эти копии, а, во-вторых, это исправит проблему.
Напиши в a.h
extern const std::string Foo;
И в a.cpp
const std::string Foo = "foo";


Для более сложных случаев есть и другие методы, вроде уже упомянутой инициализации константы как static внутри функции, но они также направлены в первую очередь на избавление от множественных копий Foo. Ибо если копия одна, то в той же единице трансляции с ней можно без проблем работать.
Re: Порядок инициализации статических переменных
От: visual_wind  
Дата: 11.12.13 11:26
Оценка: 2 (2)
Здравствуйте, Посторонним В., Вы писали:

ПВ>Кто-нибудь может объяснить, верно ли такое поведение?

ПВ>Как лучше всего избегать такого рода ошибок?

Этот момент неплохо описан, например, у Джефа Элджера

Лучший выход — сделать так, чтобы программа не рассчитывала на конкретный порядок
инициализации файлов .срр. Для этого используется стандартный прием — в заголовочном файле .h
определяется глобальный объект со статической переменной, содержащей количество
инициализированных файлов .срр. При переходе от 0 к 1 вызывается функция, которая инициализирует
все глобальные объекты библиотечного файла .срр. При переходе от 1 к 0 все объекты этого файла
уничтожаются.
// В файле Library.h
class Library {
private:
    static int count;
    static void OpenLibrary();
    static void CloseLibrary();
public:
    Library();
    ~Library();
};

static Library LibraryDummy;

inline Library::Library()
{
    if (count++ == 0)
    OpenLibrary();
}

inline Library::~Library()
{
    if (--count == 0)
    CloseLibrary();
}

// В Library.cpp
int Library::count = 0; // Делается перед выполнением вычислений
int aGlobal;
Foo* aGlobalFoo;

void Library::OpenLibrary()
{
    aGlobal = 17;
    aGlobalFoo = new Foo;
}

void Library::CloseLibrary()
{
    aGlobal = 0;
    delete aGlobalFoo;
    aGlobalFoo = NULL;
}

К этому нужно привыкнуть. А происходит следующее: файл .h компилируется со множеством других
файлов .срр, один из которых - Library.cpp. Порядок инициализации глобальных объектов,
встречающихся в этих файлах, предсказать невозможно. Тем не менее, каждый из них будет иметь
свою статическую копию LibraryDummy. При каждой инициализации файла .срр, в который включен
файл Library.h, конструктор LibraryDummy увеличивает счетчик. При выходе из main() или при
вызове exit() файлы .срр уничтожают глобальные объекты и уменьшают счетчик в деструкторе
LibraryDummy. Конструктор и деструктор гарантируют, что OpenLibrary() и CloseLibrary()
будут вызваны ровно один раз.
Этот прием приписывается многим разным программистам, но самый известный пример его
использования встречается в библиотеке iostream. Там он инициализирует большие структуры данных,
с которыми работает библиотека, ровно один раз и лишь тогда, когда это требуется.
Re[2]: Порядок инициализации статических переменных
От: Посторонним В. Беларусь  
Дата: 11.12.13 11:39
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Здравствуйте, Посторонним В., Вы писали:



W> Тут нарушение ODR. Функция getFoo() имеет external linkage, но компилируется в каждой единице трансляции (из-за inline). В то же время переменная Foo имеет internal linkage (из-за const) и в каждой единице трансляции есть своя копия. В результате получается, что при инициализации Default_Foo вызывается getFoo, но не обязательно из той же единицы трансляции.


А можно подробнее объяснить выделенное?
В этом моя ошибка, но, как же это неочевидно!
Re[2]: Порядок инициализации статических переменных
От: watchmaker  
Дата: 11.12.13 11:41
Оценка: 1 (1)
Здравствуйте, watchmaker, Вы писали:


ПВ>>Как лучше всего избегать такого рода ошибок?

W>Можно просто избавится от множества копий Foo. Во-первых, сомнительно, что тебе вообще нужные эти копии, а, во-вторых, это исправит проблему.
Либо можно избавится от множества функций getFoo, убрав inline и переместив её определение в a.cpp. Если функций одна, то и работать она будет только со своим экземпляром Foo, который уже будет инициализирован. Разумеется, остальные копии Foo никуда не денутся. Но других проблем (кроме факта своего бессмысленного существования) они не создадут пока к ним обращаются не напрямую, а только через getFoo.
Поэтому всё-таки надёжнее выпиливать копии Foo, чем копии getFoo() — меньше шанс, что что-то сломается при будущем рефакторинге.
Re[3]: Порядок инициализации статических переменных
От: watchmaker  
Дата: 11.12.13 12:00
Оценка: 3 (1)
Здравствуйте, Посторонним В., Вы писали:

ПВ>Здравствуйте, watchmaker, Вы писали:


W>>Здравствуйте, Посторонним В., Вы писали:



W>> Тут нарушение ODR. Функция getFoo() имеет external linkage, но компилируется в каждой единице трансляции (из-за inline). В то же время переменная Foo имеет internal linkage (из-за const) и в каждой единице трансляции есть своя копия. В результате получается, что при инициализации Default_Foo вызывается getFoo, но не обязательно из той же единицы трансляции.


ПВ>А можно подробнее объяснить выделенное?

Формально так:

There can be more than one definition of a class type (Clause 9), enumeration type (7.2), inline function with
external linkage (7.1.2)

— in each definition of D, corresponding names, looked up according to 3.4, shall refer to an entity defined
within the definition of D, or shall refer to the same entity, after overload resolution (13.3) and after
matching of partial template specialization (14.8.3), except that a name can refer to a const object
with internal or no linkage if the object has the same literal type in all definitions of D, and the object
is initialized with a constant expression (5.19), and the value (but not the address) of the object is
used, and the object has the same value in all definitions of D;

If the definitions of D
do not satisfy these requirements, then the behavior is undefined.


На практике компилятор компилирует каждый модуль отдельно. В результате в каждом модуле есть своя getFoo. Аналогично в каждом модуле, где используется std::sort<int> над обычным массивом, будет своя реализация сортировки. Но нет смысла их включать в выходной файл — они все эквивалентны. Поэтому при сборке linker может взять любую реализацию, ибо они все равны. У тебя же получилось сделать в каждом модуле разные getFoo.
Re[4]: Порядок инициализации статических переменных
От: Кодт Россия  
Дата: 11.12.13 13:00
Оценка: 1 (1) +1
Здравствуйте, watchmaker, Вы писали:

W>На практике компилятор компилирует каждый модуль отдельно. В результате в каждом модуле есть своя getFoo. Аналогично в каждом модуле, где используется std::sort<int> над обычным массивом, будет своя реализация сортировки. Но нет смысла их включать в выходной файл — они все эквивалентны. Поэтому при сборке linker может взять любую реализацию, ибо они все равны. У тебя же получилось сделать в каждом модуле разные getFoo.


Мало того, что линкер возьмёт первую попавшуюся копию, так ещё и компилятор некоторые вызовы проинлайнит или сделает напрямую, а некоторые — сделает через внешнюю связь (см. линкер).
Поэтому даже в пределах одного модуля можно иметь дело с двумя разными реализациями одной функции.
http://files.rsdn.org/4783/catsmiley.gif Перекуём баги на фичи!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.