Программа курса← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

static, inline и единицы трансляции

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), компоновщик оставил одно определение, и половина программы работает с чужой раскладкой класса. Диагностики нет, падение случайное. Отсюда важность одинаковых флагов компиляции для всех файлов проекта.