Столько возни! Радует, что после выхода C++17 делать это стало гораздо проще.
Подключаем библиотеки с помощью встраиваемых переменных
Несмотря на то, что в C++ всегда была возможность определить отдельные функции как встраиваемые, C++17 дополнительно позволяет определять встраиваемые переменные. Это значительно упрощает реализацию библиотек, размещенных в заголовочных файлах, для чего раньше приходилось искать обходные пути.
Как это делается
В этом примере мы создаем класс-пример, который может служить членом типичной библиотеки, размещенной в заголовочном файле. Мы хотим предоставить доступ к статическому полю класса через глобально доступный элемент класса и сделать это с помощью ключевого слова inline
, что до появления C++17 было невозможно.
1. Класс process_monitor
должен содержать статический член и быть доступным глобально сам по себе, что приведет (при включении его в несколько единиц трансляции) к появлению символов, определенных дважды:
// foo_lib.hpp
class process_monitor {
public:
static const std::string standard_string
{"some static globally available string"};
};
process_monitor global_process_monitor;
2. Теперь при попытке включить данный код в несколько файлов с расширением .cpp
, а затем скомпилировать и связать их произойдет сбой на этапе связывания. Чтобы это исправить, добавим ключевое слово inline
:
// foo_lib.hpp
class process_monitor {
public:
static const inline std::string standard_string
{"some static globally available string"};
};
inline process_monitor global_process_monitor;
Вуаля! Все работает!
Как это работает
Программы, написанные на C++, зачастую состоят из нескольких исходных файлов C++ (они имеют расширения .cpp
или .cc
). Они отдельно компилируются в модули/объектные файлы (обычно с расширениями .o
). На последнем этапе все эти модули/объектные файлы компонуются в один исполняемый файл или разделяемую/статическую библиотеку.
На этапе связывания ошибкой считается ситуация, когда компоновщик встречает вхождение одного конкретного символа несколько раз. Предположим, у нас есть функция с сигнатурой int foo();
. Если в двух модулях определены одинаковые функции, то какую из них считать правильной? Компоновщик не может просто подбросить монетку. Точнее, может, но вряд ли хоть один программист сочтет такое поведение приемлемым.
Традиционный способ создания функций, доступных глобально, состоит в объявлении их в заголовочном файле, впоследствии включенном в любой модуль С++, в котором их нужно вызвать. Эти функции будут определяться в отдельных файлах модулей. Далее они связываются с теми модулями, которые должны использовать эти функции. Данный принцип также называется правилом одного определения (one definition rule, ODR). Взгляните на рис. 1.1, чтобы лучше понять это правило.
Однако будь это единственный способ решения задачи, нельзя было бы создавать библиотеки, размещенные в заголовочных файлах. Такие библиотеки очень удобны, поскольку их можно включить в любой файл программы С++ с помощью директивы #include
, и они мгновенно станут доступны. Для использования же библиотек, размещенных не в заголовочных файлах, программист также должен адаптировать сценарии сборки так, чтобы компоновщик связал модули библиотек и файлы своих модулей. Это неудобно, особенно для библиотек, содержащих только очень короткие функции.
В таких случаях можно применить ключевое слово inline
— оно позволяет в порядке исключения разрешить повторяющиеся определения одного символа в разных модулях. Если компоновщик находит несколько символов с одинаковой сигнатурой, но они объявлены встраиваемыми, то он выберет первый и будет считать, что остальные символы имеют такое же определение. На программиста возложена ответственность за то, чтобы все одинаковые встраиваемые символы были определены абсолютно идентично.