C++ славен тем, что почти все его конструкции невероятно сильно зависят от контекста, и просто, взглянув на случайный участок кода, крайне сложно быть уверенным в понимании того, что же он делает. Перегруженные операторы, контекстно-зависимые значения ключевых слов, ADL, auto
, auto
, auto
!
Одно из самых перегруженных значениями ключевых слов в C++ — static
.
static
это и модификатор видимости, влияющий на линковку,static
это и storage-модификатор, влияющий на то, где и как долго переменная будет храниться,static
это еще и модификатор, влияющий на то, как переменная или метод, ассоциированные с классом или структурой, будут взаимодействовать с объектами этих типов.
В C++23 будут еще и static перегрузки для operator()
! Это будет что-то новое, восхитительное и прекрасное.
Главное — не путать со static модификатором при перегрузке других операторов вне класса. Ведь это уже модификатор видимости! И если написать в разных единицах трансляции что-нибудь вот такое
/// TU1.cpp
static Monoid operator + (Monoid a, Monoid b) {
return {
a.value + b.value
};
}
Monoid sum(Monoid a, Monoid b) {
return a + b;
}
/// TU2.cpp
static Monoid operator + (Monoid a, Monoid b) {
return {
a.value * b.value
};
}
Monoid mult(Monoid a, Monoid b) {
return a + b;
}
/// main.cpp
int main(int argc, char **argv) {
auto v1 = sum({5}, {6}).value;
auto v2 = mult({5}, {6}).value;
std::cout << v1 << " " << v2 << "\n";
}
То оно даже будет работать ожидаемым образом. Ведь никакой проблемы нет — определения локальны в единицах трансляции.
В C++17 дополнительными значениями обросло еще и ключевое слово inline
.
Когда-то оно было лишь подсказкой компилятору, что тело функции надо "встраивать" вместо вызова — то есть не делать относительно дорогой call
с сохранением точки возврата, регистров, еще чего-то, прямо вместо вызова воткнуть инструкции... Подсказка эта, правда, не всегда работает. По разным причинам. Но в основном потому что программисты писали и пишут ее налево и направо даже туда, куда этого делать не стоит, чтобы не раздувать чрезмерно получаемый код. Но это не наша история. Наша история о другом.
В современном C++ inline
используется чаще всего только для того, чтоб поместить определение функции в заголовочный файл.
В C это тоже работает, но совсем не так — вместо ошибки multiple definition, к которой приводит помещение не-inline функций в заголовочный файл и которую мы хотели избежать, мы вовсе получили undefined reference.
В C inline
определения из заголовка нужно сопрячь модификатором static
. И, возможно, получить code bloating, потому что получите копию функции в каждой единице трансляции и все они будут считаться разными, если линковщик окажется недостаточно умным.
Либо все-таки предоставить одно не-inline определение где-нибудь. Например, вот таким мерзким трюком
// square.h
#ifdef DEFINE_STUB
#define INLINE
#else
#define INLINE inline
#endif
INLINE int square(int num) {
return num * num;
}
// square.c
#define DEFINE_STUB
#include "square.h"
// main.c
#include "square.h"
int main() {
return square(5);
}
Или же упомянуть где-нибудь объявление этой функции со спецификатором extern
(или даже без него может работать)
// square.h
inline int square(int num) {
return num * num;
}
// square.c
#include "foo.h"
extern int square(int num);
// main.c
#include "foo.h"
int main() {
return square(5);
}
Либо, пользуясь GCC, никогда не собирать сишный код без оптимизаций. Я таких разработчиков тоже видел. Но работает это решение не всегда
// square.h
inline int square(int num) {
return num * num;
}
inline int cube(int num) {
return num * num * num;
}
// main.c
#include "square.h"
#include <stdlib.h>
typedef int (*fn) (int);
int main() {
fn f;
if (rand() % 2) {
f = square;
} else {
f = cube;
}
// адреса inline-функции не известны -> undefined reference
return f(5);
}
Но вернемся к C++. Помимо функций в заголовках иногда очень хочется определять еще и переменные. В приличных проектах, конечно, в основном константы. Но разработка сложна, туманна и полна ужасов. А также нестандартных креативных решений, которые пришлось принять здесь и сейчас. Поэтому встречаются не только константы.
К сожалению, в C++ до 17 стандарта просто так взять и поместить в заголовочный файл определение какой-то константы было не всегда возможно. А если и возможно, то с интересными спецэффектами.
// my_class.hpp
struct MyClass {
static const int max_limit = 5000;
};
// main.cpp
#include "my_class.hpp"
#include <algorithm>
int main() {
int limit = MyClass::max_limit; // OK
return std::min(5, MyClass::max_limit); // Compilation error! std::min хочет принять ссылку, но линкер не знает адрес этой константы!
}
Можно написать
// my_class.hpp
struct MyClass {
static constexpr int max_limit = 5000;
};
И оно заработает
Но constexpr
возможен не всегда и тогда все-таки придется взять и отнести определение в отдельную единицу трансляции...
Пришел C++17 и нашим мучениям настал конец! Теперь можно написать inline
у переменной и компилятор это съест, сгенерирует подобающую аннотацию для символа в объектном файле, чтоб линковщик более не кричал на multiple definition. Пусть берет любое, мы гарантируем что все определения одинаковые, а иначе undefined behavior.
// my_class.hpp
#include <unordered_map>
#include <string>
struct MyClass {
static const inline
std::unordered_map<std::string, int> supported_types_versions = {
{"int", 5},
{"string", 10}
};
};
inline const std::unordered_map<std::string, int> another_useful_map = {
{"int", 5},
{"string", 6}
};
void test();
// my_class.cpp
#include "my_class.hpp"
#include <iostream>
void test() {
std::cout << another_useful_map.size() << "\n";
}
// main.cpp
#include "my_class.hpp"
#include <algorithm>
#include <iostream>
int main() {
std::cout << MyClass::supported_types_versions.size() << "\n";
test();
}
Все прекрасно работает — никаких multiple definitions и никаких undefined references! Невероятно похорошел C++ при 17-ом стандарте!
Внимательный читатель уже должен был почувствовать и даже заметить подвох.
Вот перед вами блок кода
DEFINE_NAMESPACE(details)
{
class Impl { ... };
static int process(Impl);
static inline const std::vector<std::string> type_list = { ... };
};
Может ли что-то пойти не так?
Конечно же может! Это же C++!
DEFINE_NAMESPACE(name)
может быть определен как
#define DEFINE_NAMESPACE(name) namespace name
А может быть как
#define DEFINE_NAMESPACE(name) struct name
Что?! Да! Что если из благих побуждений, чтоб спрятать доступ к перегрузке функции process
от вездесущего ADL, однажды сумрачному гению автора библиотеки пришло в голову именно такое решение, которое включается и выключается всего одним макросом!
В таких случаях вообще-то type_list
это разные вещи.
В случае namespace
это static inline
глобальная переменная. inline
тут как бы бесполезен, потому что static
и глобальной переменной модифицирует видимость (linkage). В каждой единице трансляции, в которой окажется такой заголовок подключенным, будет своя копия переменной type_list
.
В случае же class
или struct
этот static inline
поле, ассоциированное с классом. И оно будет одно на всех.
Ну ладно, какая разница! Они же константы и объявлены одинаково! Никто ничего не заметит на практике... Разумеется.
А теперь мы вспоминаем, что иногда нам нужны не константы. Например, если мы опять-таки делаем эту избитую систему с автоматической регистрацией плагинов при загрузке библиотек или иную систему авторегистрации типов.
Вот так все работает. Красиво и ожидаемо.
// plugin_storage.h
#include <vector>
#include <string>
using PluginName = std::string;
struct PluginStorage {
static inline std::vector<PluginName> registered_plugins;
};
// plugin.cpp
#include "plugin_storage.h"
namespace {
struct Registrator {
Registrator() {
PluginStorage::registered_plugins.push_back("plugin");
}
} static registrator_;
}
// main.cpp
#include "plugin_storage.h"
#include <iostream>
int main() {
// печатает ровно один элемент
for (auto&& p : PluginStorage::registered_plugins) {
std::cout << p << "\n";
}
}
Но меняем struct PluginStorage
на namespace PluginStorage
— все компилируется, но уже не работает. Переменная PluginStorage
своя в каждой единицу трансляции, поэтому в main
мы видим пустой список.
Нужно удалить static
перед inline
и мы получим желаемое поведение снова.
Изменяемые глобальные статические переменные это сложно везде. В Rust, например, обращение к ним обязательно требует unsafe
.
C++ ничего не требует. Вам нужно самим помнить о множественных синтаксических ритуалах, которые нужно произвести.
- Спрятать в функцию, чтоб избежать static initialization order fiasco
- Не написать лишних
static
- Не запихнуть по неосторожности в заголовочный файл
- Максимально ограничить доступ
И еще не забыть про многопоточный доступ.
С++17 породил static inline
переменные. Они удобные. Но только когда неизменяемые. Хотя и не беспроблемные.
Средства просмотра изменений на ревью могут не показывать весь файл, а только лишь часть с добавлением. Если видите static inline
не забудьте посмотреть, в каком он контексте. Если это проигнорировать в лучшем случае ваши исполняемые файлы будут тяжелыми. В худшем — можно уйти во многие часы безнадежной отладки после какого-нибудь минималистичного изменения: кто-то объявление переменной с глобальным состоянием в заголовок вынес или наоборот внес, логически же ничего не поменялось...
Изменяемые статики — страшное зло. С ними не только у рядовых разработчиков проблемы.
На момент написания этой статьи в clang имеется баг с порядком инициализации статиков внутри одной единицы трансляции. Из-за неправильной сортировки static
глобальных переменных и static inline
полей классов.