Инициализация статических переменных в С++. Неоднозначности и возможные решения | OTUS

Инициализация статических переменных в С++. Неоднозначности и возможные решения

otus_Posts_25may_VK_1000x700_2-20219-86ca43.jpg

Занимаясь разработкой на языке С++, мы рано или поздно приходим к вопросу об инициализации статических переменных. Более сложный случай, когда проект большой и над ним трудится много людей. Статические переменные, как правило, объявляются в глобальной области имён, то есть используются многими участниками проекта, что часто вызывает споры и раздоры.

Есть мнение о полном избегании объявления глобальных переменных в угоду обеспечения низкой связанности и манипуляции взаимодействия классов. Давайте последовательно посмотрим, что же происходит.

1. Статическая инициализация

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

Как пример:

const std :: size_t tabsize = 64 ;
int tab [ tabsize ] ;

2. Нулевая инициализация

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

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

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

Но здесь есть свои подводные камни, о которых необходимо упомянуть. Мы все любим выражения подобно auto VER = std::string( "3.4.1" ), так вот, выражения подобного вида с constexpr, например:

constexpr auto VER = std::string( "3.4.1" ); 

работать не будут, что и логично, так как класс std::string выделяет некоторый ресурс, который должен быть освобождён при уничтожении, в данном случае памяти. Следовательно, std::string( "3.4.1" ) не может быть константным выражением, вычисляемым во время компиляции. В замен мы вынуждены использовать const и за это платим перемещением из времени компиляции во время выполнения, т. е. переходим из статической в динамическую инициализацию.

const auto VER = std::string( "3.4.1" ); 

3. Static-проблема порядка инициализации

Так как порядок инициализации статических переменных чётко не определён, возникает серьёзная проблема правильной инициализации, если значения находятся в разных модулях. Короче говоря, предположим, что у нас есть два static-объекта x и y, которые существуют в отдельных исходных файлах, скажем, x.cpp и y.cpp. Предположим далее, что инициализация для y объекта (обычно y — конструктор объекта) вызывает некоторый метод x объекта. Вот и все. Мы получили 50%-ную вероятность испортить программу.

Как правило, подобная проблема возникает из-за плохого проектирования проекта. Лучший способ её решить — это рефакторинг кода, чтобы разорвать зависимость инициализации глобальных переменных от единиц компиляции. Необходимо сделать модули автономными и стремиться к постоянной инициализации.

Если рефакторинг кода не подходит, возможно использовать идиому Construct On First Use. Основная идея состоит в том, чтобы спроектировать статические переменные, которые не являются константными выражениями (то есть теми, которые должны быть инициализированы во время выполнения) таким образом, чтобы они создавались при первом обращении к ним. Подобный подход часто называется синглтоном Мейера. Как пример:

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

B всегда будет инициализироваться в значение 14.

В заключении хотелось бы отметить, что в общем смысле вопрос правильной инициализации достаточно сложный. Этой теме посвящено множество публикаций и выступлений, особенно на фоне неоднозначного изменения в стандартах языка С++. Так, C++11 принёс концепцию «универсальной инициализации», которая привнесла ещё более сложные правила, и, в свою очередь, их перекрыли в C++14, C++17 и снова поменяют в C++20.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто