This lesson is not translated yet – the Russian original is below.
static – самое перегруженное ключевое слово в C++: четыре разных смысла в зависимости от того,
где оно написано. Разбираться с ним удобно вместе с inline, потому что оба про одно – про то,
как файлы собираются в программу.
Напоминание про единицы трансляции
Компилятор обрабатывает каждый .cpp независимо: подставляет заголовки, получает единицу
трансляции, выдаёт объектный файл. Про остальные файлы он в этот момент не знает ничего.
Связывает их компоновщик – по именам символов.
Правило одного определения (ODR): объявлений у сущности может быть сколько угодно, определение – ровно одно на программу.
// foo.hpp
void Foo(int value) { std::cout << value; } // определение в заголовке
Три .cpp, включившие этот заголовок, дадут три определения Foo – и компоновщик откажется
собирать программу.
static: внутреннее связывание
Первый смысл – у функции или переменной на уровне файла:
// utils.cpp
static int g_cache = 0; // видна только в этом файле
static void Helper() { /* ... */ } // тоже
Такие символы наружу не выставляются: другой файл не увидит их и не столкнётся с ними по имени.
// foo.hpp
static void Foo(int value) { std::cout << value; }
Теперь три .cpp соберутся – но получат три независимые функции, у каждой своя копия кода и
свои статические переменные. Обычно это не то, чего хотели.
В современном C++ для внутреннего связывания предпочитают анонимное пространство имён: namespace { int g_cache = 0; }. Работает так же, но действует и на типы, для которых static не применим.
static: время жизни
Второй смысл – переменная внутри функции:
int NextId() {
static int counter = 0; // создаётся один раз, живёт до конца программы
return ++counter;
}
Такая переменная лежит не на стеке, а в сегменте статических данных: между вызовами значение сохраняется. Инициализируется она при первом заходе в функцию, и в C++11 эта инициализация гарантированно потокобезопасна – на этом построена «ленивая» реализация одиночки, к которой мы придём в конце модуля.
static: член класса
Третий смысл – поле или метод, принадлежащие классу, а не объекту:
class Widget {
public:
static int Count() { return count_; } // нет this, вызывается как Widget::Count()
Widget() { ++count_; }
~Widget() { --count_; }
private:
static int count_; // объявление
};
int Widget::count_ = 0; // определение – ровно одно, в .cpp
Статический метод не получает this, поэтому не может обращаться к обычным полям – зато его можно
вызвать, не имея ни одного объекта.
Отдельное определение поля в .cpp – следствие того же ODR: в заголовке стоит только объявление.
С C++17 этого можно избежать:
class Widget {
static inline int count_ = 0; // определение прямо здесь
};
static: наследие C
Четвёртый смысл – static у параметра массива в объявлении функции. Экзотика, унаследованная из
C99; в C++ встречается разве что в чужих заголовках.
inline
Изначально inline был просьбой встроить тело функции в место вызова. Сегодня решение о
встраивании компилятор принимает сам, и inline означает совсем другое: разрешение на
несколько идентичных определений.
// foo.hpp
inline void Foo(int value) { std::cout << value; }
Каждая единица трансляции получит определение, компоновщик оставит одно. Именно это позволяет класть в заголовки короткие функции.
// foo.hpp
inline int Count() {
static int counter = 0;
return ++counter;
}
Здесь функция одна и счётчик один. Сравните со static-версией: три функции и три счётчика.
| В заголовке | Сколько функций в программе | Сколько статических переменных внутри |
|---|---|---|
| без ключевого слова | ошибка компоновки | – |
static |
по одной на файл | по одной на файл |
inline |
одна | одна |
inline static |
по одной на файл | по одной на файл |
Последняя строка – ловушка: static задаёт внутреннее связывание и перебивает inline. Сочетание почти всегда написано по ошибке, а компилятор о нём молчит.
Методы, определённые прямо в теле класса, неявно inline – поэтому их можно писать в заголовке
без всяких ключевых слов. Шаблоны на ODR тоже не наталкиваются: их определения обязаны быть
видны в каждой единице трансляции, где они инстанцируются.
Практические выводы
Что можно класть в заголовок: объявления функций, определения классов, шаблоны, inline- и
constexpr-функции, inline-переменные (C++17).
Что нельзя: обычные определения функций и переменных с внешним связыванием. Для глобальной
переменной – extern в заголовке, определение в одном .cpp:
// config.hpp
extern int g_verbosity; // объявление
// config.cpp
int g_verbosity = 0; // определение
Нарушение ODR через «одинаковые» определения, которые на самом деле различаются, – самый неприятный класс ошибок сборки: два файла включили разные версии заголовка (например, из-за разных #define), компоновщик оставил одно определение, и половина программы работает с чужой раскладкой класса. Диагностики нет, падение случайное. Отсюда важность одинаковых флагов компиляции для всех файлов проекта.