Files
ubbook/runtime/static_inline.md
2024-09-27 23:59:53 +01:00

299 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# static inline
C++ славен тем, что почти все его конструкции невероятно сильно зависят от контекста, и просто, взглянув на случайный участок кода, крайне сложно быть уверенным в понимании того, что же он делает. Перегруженные операторы, контекстно-зависимые значения ключевых слов, ADL, `auto`, `auto`, `auto`!
Одно из самых перегруженных значениями ключевых слов в C++ — `static`.
- `static` это и модификатор видимости, влияющий на линковку,
- `static` это и storage-модификатор, влияющий на то, где и как долго переменная будет храниться,
- `static` это еще и модификатор, влияющий на то, как переменная или метод, ассоциированные с классом или структурой, будут взаимодействовать с объектами этих типов.
В C++23 [будут](https://wg21.link/P1169R4) еще и static перегрузки для `operator()`! Это будет что-то новое, восхитительное и прекрасное.
Главное — не путать со static модификатором при перегрузке других операторов вне класса. Ведь это уже модификатор видимости!
И если написать в разных единицах трансляции что-нибудь вот такое
```C++
/// 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";
}
```
То оно даже будет [работать](https://godbolt.org/z/G5747YGK8) ожидаемым образом. Ведь никакой проблемы нет — определения локальны в единицах трансляции.
В C++17 дополнительными значениями обросло еще и ключевое слово `inline`.
Когда-то оно было лишь подсказкой компилятору, что тело функции надо "встраивать" вместо вызова — то есть не делать относительно дорогой `call` с сохранением точки возврата, регистров, еще чего-то, прямо вместо вызова воткнуть инструкции... Подсказка эта, правда, не всегда работает. По разным причинам. Но в основном потому что программисты писали и пишут ее налево и направо даже туда, куда этого делать не стоит, чтобы не раздувать чрезмерно получаемый код. Но это не наша история. Наша история о другом.
В современном C++ `inline` используется чаще всего только для того, чтоб поместить определение функции в заголовочный файл.
В C это тоже работает, но [совсем не так](https://godbolt.org/z/PeaqGr7r3) — вместо ошибки multiple definition, к которой приводит помещение не-inline функций в заголовочный файл и которую мы хотели избежать, мы вовсе получили undefined reference.
В C `inline` определения из заголовка нужно сопрячь модификатором `static`. И, возможно, получить code bloating, потому что получите копию функции в каждой единице трансляции и все они будут считаться разными, если линковщик окажется недостаточно умным.
Либо все-таки [предоставить](https://godbolt.org/z/YPvGv16fG) одно не-inline определение где-нибудь. Например, вот таким мерзким трюком
```C
// 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);
}
```
Или же [упомянуть](https://godbolt.org/z/n6q5jeqG4) где-нибудь объявление этой функции со спецификатором `extern` (или даже без него [может работать](https://godbolt.org/z/5sq7Y6rx5))
```C
// 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, никогда [не собирать](https://godbolt.org/z/EG7Kxbfov) сишный код без оптимизаций. Я таких разработчиков тоже видел. Но работает это решение [не всегда](https://godbolt.org/z/hc9MrbdY6)
```C
// 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 стандарта просто так взять и поместить в заголовочный файл определение какой-то константы было не всегда возможно. А если и возможно, то с интересными [спецэффектами](https://godbolt.org/z/W8c1YbqdE).
```C++
// 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 хочет принять ссылку, но линкер не знает адрес этой константы!
}
```
Можно написать
```C++
// my_class.hpp
struct MyClass {
static constexpr int max_limit = 5000;
};
```
И оно [заработает](https://godbolt.org/z/93cbWb7eY)
Но `constexpr` возможен не всегда и тогда все-таки придется взять и отнести определение в отдельную единицу трансляции...
Пришел C++17 и нашим мучениям настал конец! Теперь можно написать `inline` у переменной и компилятор это съест, сгенерирует подобающую аннотацию для символа в объектном файле, чтоб линковщик более не кричал на multiple definition. Пусть берет любое, мы гарантируем что все определения одинаковые, а иначе undefined behavior.
```C++
// 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();
}
```
Все прекрасно [работает](https://godbolt.org/z/W79dbxGz3) — никаких multiple definitions и никаких undefined references! Невероятно похорошел C++ при 17-ом стандарте!
Внимательный читатель уже должен был почувствовать и даже заметить подвох.
Вот перед вами блок кода
```C++
DEFINE_NAMESPACE(details)
{
class Impl { ... };
static int process(Impl);
static inline const std::vector<std::string> type_list = { ... };
};
```
Может ли что-то пойти не так?
Конечно же может! Это же C++!
`DEFINE_NAMESPACE(name)` может быть определен как
```C++
#define DEFINE_NAMESPACE(name) namespace name
```
А может быть как
```C++
#define DEFINE_NAMESPACE(name) struct name
```
Что?! Да! Что если из благих побуждений, чтоб спрятать доступ к перегрузке функции `process` от вездесущего ADL, однажды сумрачному гению автора библиотеки пришло в голову именно такое решение, которое включается и выключается всего одним макросом!
В таких случаях вообще-то `type_list` это разные вещи.
В случае `namespace` это `static inline` глобальная переменная. `inline` тут как бы бесполезен, потому что `static` и глобальной переменной модифицирует видимость (linkage). В каждой единице трансляции, в которой окажется такой заголовок подключенным, будет своя копия переменной `type_list`.
В случае же `class` или `struct` этот `static inline` поле, ассоциированное с классом. И оно будет одно на всех.
Ну ладно, какая разница! Они же константы и объявлены одинаково! Никто ничего не заметит на практике... Разумеется.
А теперь мы вспоминаем, что иногда нам нужны не константы. Например, если мы опять-таки делаем эту избитую систему с автоматической регистрацией плагинов при загрузке библиотек или иную систему авторегистрации типов.
Вот так [все работает](https://godbolt.org/z/s5aca7hce). Красиво и ожидаемо.
```C++
// 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` — все компилируется, но [уже не работает](https://godbolt.org/z/7P7aEzxjT). Переменная `PluginStorage` своя в каждой единицу трансляции, поэтому в `main` мы видим пустой список.
Нужно удалить `static` перед `inline` и мы [получим](https://godbolt.org/z/hnn5YM1Wf) желаемое поведение снова.
### Итого
Изменяемые глобальные статические переменные это сложно везде. В Rust, например, обращение к ним обязательно требует `unsafe`.
C++ ничего не требует. Вам нужно самим помнить о множественных синтаксических ритуалах, которые нужно произвести.
- Спрятать в функцию, чтоб избежать static initialization order fiasco
- Не написать лишних `static`
- Не запихнуть по неосторожности в заголовочный файл
- Максимально ограничить доступ
И еще не забыть про многопоточный доступ.
С++17 породил `static inline` переменные. Они удобные. Но только когда неизменяемые. Хотя и не беспроблемные.
Средства просмотра изменений на ревью могут не показывать весь файл, а только лишь часть с добавлением. Если видите `static inline` не забудьте посмотреть, в каком он контексте. Если это проигнорировать в лучшем случае ваши исполняемые файлы будут тяжелыми. В худшем — можно уйти во многие часы безнадежной отладки после какого-нибудь минималистичного изменения: кто-то объявление переменной с глобальным состоянием в заголовок вынес или наоборот внес, логически же ничего не поменялось...
Изменяемые статики — страшное зло. С ними не только у рядовых разработчиков проблемы.
На момент написания этой статьи в clang имеется [баг](https://github.com/llvm/llvm-project/issues/55804) с порядком инициализации статиков внутри одной единицы трансляции. Из-за неправильной сортировки `static` глобальных переменных и `static inline` полей классов.
### Полезные ссылки
1. https://en.cppreference.com/w/cpp/keyword/static
2. https://en.cppreference.com/w/cpp/language/inline