0 added
0 removed
Original
2026-01-01
Modified
2026-03-10
1
<p>Занимаясь разработкой на языке С++, мы рано или поздно приходим к вопросу об<strong>инициализации статических переменных</strong>. Более сложный случай, когда проект большой и над ним трудится много людей. Статические переменные, как правило, объявляются в глобальной области имён, то есть используются многими участниками проекта, что часто вызывает споры и раздоры.</p>
1
<p>Занимаясь разработкой на языке С++, мы рано или поздно приходим к вопросу об<strong>инициализации статических переменных</strong>. Более сложный случай, когда проект большой и над ним трудится много людей. Статические переменные, как правило, объявляются в глобальной области имён, то есть используются многими участниками проекта, что часто вызывает споры и раздоры.</p>
2
<p>Есть мнение о полном избегании объявления глобальных переменных в угоду обеспечения низкой связанности и манипуляции взаимодействия классов. Давайте последовательно посмотрим, что же происходит.</p>
2
<p>Есть мнение о полном избегании объявления глобальных переменных в угоду обеспечения низкой связанности и манипуляции взаимодействия классов. Давайте последовательно посмотрим, что же происходит.</p>
3
<h2>1. Статическая инициализация</h2>
3
<h2>1. Статическая инициализация</h2>
4
<p>При создании статической переменной, возникает вопрос, когда эта переменная будет инициализирована, сколько времени она проживёт и когда будет уничтожена? Статическая инициализация позволяет нам создать переменную, которая будет инициализирована до запуска программы со временем жизни в течение всей программы и уничтожении после завершения. Такие константные переменные не зависят от исполнения - они всегда существуют, создаются во время компиляции и располагаются в исполнимом файле (в бинарнике). Как результат: нулевые накладные расходы, ранняя диагностика проблем и безопасность. Вопрос об использовании статической инициализации напрямую зависит от предметной области разрабатываемого проекта - чем ниже уровень, тем соблазн использования выше, а иногда и критичен, тут скорость решает всё.</p>
4
<p>При создании статической переменной, возникает вопрос, когда эта переменная будет инициализирована, сколько времени она проживёт и когда будет уничтожена? Статическая инициализация позволяет нам создать переменную, которая будет инициализирована до запуска программы со временем жизни в течение всей программы и уничтожении после завершения. Такие константные переменные не зависят от исполнения - они всегда существуют, создаются во время компиляции и располагаются в исполнимом файле (в бинарнике). Как результат: нулевые накладные расходы, ранняя диагностика проблем и безопасность. Вопрос об использовании статической инициализации напрямую зависит от предметной области разрабатываемого проекта - чем ниже уровень, тем соблазн использования выше, а иногда и критичен, тут скорость решает всё.</p>
5
<p>Как пример:</p>
5
<p>Как пример:</p>
6
const std :: size_t tabsize = 64 ; int tab [ tabsize ] ;<h2>2. Нулевая инициализация</h2>
6
const std :: size_t tabsize = 64 ; int tab [ tabsize ] ;<h2>2. Нулевая инициализация</h2>
7
<p>Она возникает тогда, когда начальное значение не может быть оценено во время компиляции, в этом случае все статические переменные либо инициализируются константами, либо нулём, что является большой проблемой, так как однозначно неизвестен вид инициализации. Такая переменная попадёт в динамическую память, но с константным выражением обычно инициализированной нулём, либо она будет в бинарнике с заданным константным выражением. Как следствие, это ведёт к появлению трудно выявляемых ошибок, так как исчезает возможность контроля корректности инициализированных значений.</p>
7
<p>Она возникает тогда, когда начальное значение не может быть оценено во время компиляции, в этом случае все статические переменные либо инициализируются константами, либо нулём, что является большой проблемой, так как однозначно неизвестен вид инициализации. Такая переменная попадёт в динамическую память, но с константным выражением обычно инициализированной нулём, либо она будет в бинарнике с заданным константным выражением. Как следствие, это ведёт к появлению трудно выявляемых ошибок, так как исчезает возможность контроля корректности инициализированных значений.</p>
8
<p>Для того чтобы этого избежать и принудительно создать статическую переменную, используется спецификатор<em>constexpr</em>, который вычисляет выражение (или результат работы функции) на этапе компиляции при условии, что оно может быть вычислено. Например:</p>
8
<p>Для того чтобы этого избежать и принудительно создать статическую переменную, используется спецификатор<em>constexpr</em>, который вычисляет выражение (или результат работы функции) на этапе компиляции при условии, что оно может быть вычислено. Например:</p>
9
constexpr auto const getLog(std::size_t n){ std :: size_t k = 0; while(n>>= 1) k++; return k; } constexpr std :: size_t n = 64 ; constexpr std :: size_t sz = getLog(tabsize) ; int tab2 [ sz ] ;<p>Но здесь есть свои подводные камни, о которых необходимо упомянуть. Мы все любим выражения подобно auto VER = std::string( "3.4.1" ), так вот, выражения подобного вида с<em>constexpr</em>, например:</p>
9
constexpr auto const getLog(std::size_t n){ std :: size_t k = 0; while(n>>= 1) k++; return k; } constexpr std :: size_t n = 64 ; constexpr std :: size_t sz = getLog(tabsize) ; int tab2 [ sz ] ;<p>Но здесь есть свои подводные камни, о которых необходимо упомянуть. Мы все любим выражения подобно auto VER = std::string( "3.4.1" ), так вот, выражения подобного вида с<em>constexpr</em>, например:</p>
10
constexpr auto VER = std::string( "3.4.1" );<p>работать не будут, что и логично, так как класс<em>std::string</em>выделяет некоторый ресурс, который должен быть освобождён при уничтожении, в данном случае памяти. Следовательно,<em>std::string( "3.4.1" )</em>не может быть константным выражением, вычисляемым во время компиляции. В замен мы вынуждены использовать<em>const</em>и за это платим перемещением из времени компиляции во время выполнения, т. е. переходим из статической в динамическую инициализацию.</p>
10
constexpr auto VER = std::string( "3.4.1" );<p>работать не будут, что и логично, так как класс<em>std::string</em>выделяет некоторый ресурс, который должен быть освобождён при уничтожении, в данном случае памяти. Следовательно,<em>std::string( "3.4.1" )</em>не может быть константным выражением, вычисляемым во время компиляции. В замен мы вынуждены использовать<em>const</em>и за это платим перемещением из времени компиляции во время выполнения, т. е. переходим из статической в динамическую инициализацию.</p>
11
const auto VER = std::string( "3.4.1" );<h2>3. Static-проблема порядка инициализации</h2>
11
const auto VER = std::string( "3.4.1" );<h2>3. Static-проблема порядка инициализации</h2>
12
<p>Так как порядок инициализации статических переменных чётко не определён, возникает серьёзная проблема правильной инициализации, если значения находятся в разных модулях. Короче говоря, предположим, что у нас есть два static-объекта<em>x</em>и<em>y</em>, которые существуют в отдельных исходных файлах, скажем,<em>x.cpp</em>и<em>y.cpp</em>. Предположим далее, что инициализация для<em>y</em>объекта (обычно<em>y</em>- конструктор объекта) вызывает некоторый метод<em>x</em>объекта. Вот и все. Мы получили 50%-ную вероятность испортить программу.</p>
12
<p>Так как порядок инициализации статических переменных чётко не определён, возникает серьёзная проблема правильной инициализации, если значения находятся в разных модулях. Короче говоря, предположим, что у нас есть два static-объекта<em>x</em>и<em>y</em>, которые существуют в отдельных исходных файлах, скажем,<em>x.cpp</em>и<em>y.cpp</em>. Предположим далее, что инициализация для<em>y</em>объекта (обычно<em>y</em>- конструктор объекта) вызывает некоторый метод<em>x</em>объекта. Вот и все. Мы получили 50%-ную вероятность испортить программу.</p>
13
<p>Как правило, подобная проблема возникает из-за плохого проектирования проекта. Лучший способ её решить - это рефакторинг кода, чтобы разорвать зависимость инициализации глобальных переменных от единиц компиляции. Необходимо сделать модули автономными и стремиться к постоянной инициализации.</p>
13
<p>Как правило, подобная проблема возникает из-за плохого проектирования проекта. Лучший способ её решить - это рефакторинг кода, чтобы разорвать зависимость инициализации глобальных переменных от единиц компиляции. Необходимо сделать модули автономными и стремиться к постоянной инициализации.</p>
14
<p>Если рефакторинг кода не подходит, возможно использовать идиому<strong>Construct On First Use</strong>. Основная идея состоит в том, чтобы спроектировать статические переменные, которые не являются константными выражениями (то есть теми, которые должны быть инициализированы во время выполнения) таким образом, чтобы они создавались при первом обращении к ним. Подобный подход часто называется<strong>синглтоном Мейера</strong>. Как пример:</p>
14
<p>Если рефакторинг кода не подходит, возможно использовать идиому<strong>Construct On First Use</strong>. Основная идея состоит в том, чтобы спроектировать статические переменные, которые не являются константными выражениями (то есть теми, которые должны быть инициализированы во время выполнения) таким образом, чтобы они создавались при первом обращении к ним. Подобный подход часто называется<strong>синглтоном Мейера</strong>. Как пример:</p>
15
// a.cpp int duplicate ( int n ) { return n * 2 ; } auto & A () { static auto a = duplicate ( 7 ); return a; } // b.cpp #include <iostream> #include "a.h" auto B = A (); int main () { std :: cout << B << std :: endl ; return EXIT_SUCCESS ; }<p>B всегда будет инициализироваться в значение 14.</p>
15
// a.cpp int duplicate ( int n ) { return n * 2 ; } auto & A () { static auto a = duplicate ( 7 ); return a; } // b.cpp #include <iostream> #include "a.h" auto B = A (); int main () { std :: cout << B << std :: endl ; return EXIT_SUCCESS ; }<p>B всегда будет инициализироваться в значение 14.</p>
16
<p>В заключении хотелось бы отметить, что в общем смысле вопрос правильной инициализации достаточно сложный. Этой теме посвящено множество публикаций и выступлений, особенно на фоне неоднозначного изменения в стандартах языка С++. Так, C++11 принёс концепцию "универсальной инициализации", которая привнесла ещё более сложные правила, и, в свою очередь, их перекрыли в C++14, C++17 и снова поменяют в C++20.</p>
16
<p>В заключении хотелось бы отметить, что в общем смысле вопрос правильной инициализации достаточно сложный. Этой теме посвящено множество публикаций и выступлений, особенно на фоне неоднозначного изменения в стандартах языка С++. Так, C++11 принёс концепцию "универсальной инициализации", которая привнесла ещё более сложные правила, и, в свою очередь, их перекрыли в C++14, C++17 и снова поменяют в C++20.</p>
17
17