Шрифт:
Интервал:
Закладка:
}
return copy;
}
};
Без конструкций constexpr-if этот класс работает для всех необходимых нам типов, но кажется очень сложным. Как же он работает?
Сами реализации двух разных функций add выглядят просто. Все усложняет объявление возвращаемого типа — выражение наподобие std::enable_if_t<условие, тип> обращается в тип, если выполняется условие. В противном случае выражение std::enable_if_t ни во что не обращается. Обычно такое положение дел считается ошибкой. Далее мы рассмотрим, почему в нашем случае это не так.
Для второй функции add то же условие используется противоположным образом. Следовательно, условие может иметь значение true только для одной из двух реализаций в любой момент времени.
Когда компилятор видит разные шаблонные функции с одинаковым именем и должен выбрать одну из них, в ход вступает важный принцип: он обозначается аббревиатурой SFINAE, которая расшифровывается как Substitution Failure is not an Error («Сбой при подстановке — не ошибка»). В данном случае это значит, что компилятор не генерирует ошибку, если возвращаемое значение одной из функций нельзя вывести на основе неверного шаблонного выражения (т.е. std::enable_if, когда условие имеет значение false). Он просто продолжит работу и попробует обработать другие реализации функции. Вот и весь секрет.
Столько возни! Радует, что после выхода 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 — оно позволяет в порядке исключения разрешить повторяющиеся определения одного символа в разных модулях. Если компоновщик находит несколько символов с одинаковой сигнатурой, но они объявлены встраиваемыми, то он выберет первый и будет считать, что остальные символы имеют такое же определение. На программиста возложена ответственность за то, чтобы все одинаковые встраиваемые символы были определены абсолютно идентично.
Что касается нашего примера, компоновщик найдет символ process_monitor::standard_string в каждом модуле, который включает файл foo_lib.hpp. Без ключевого слова inline он не будет знать, какой символ выбрать, так что прекратит работу и сообщит об ошибке. Это же верно и для символа global_process_monitor. Как же выбрать правильный символ?
При объявлении обоих символов с помощью ключевого слова inline компоновщик просто примет первое вхождение символа и отбросит остальные.
До появления C++17 единственным явным способом сделать это было предоставление символа с помощью дополнительного файла модуля C++, что заставляло пользователей библиотеки включать данный файл на этапе компоновки.
Ключевое слово inline по традиции выполняет и другую задачу. Оно указывает компилятору, что он может избавиться от вызова функции, взяв ее реализацию и поместив в то место, из которого функция вызывается. Таким образом, вызывающий код содержит на один вызов функции меньше — считается, что такой код работает быстрее. Если функция очень короткая, то полученный ассемблерный код также будет короче (предполагается, что количество инструкций, которые выполняют вызов функции, сохранение и восстановление стека и т.д., превышает количество строк с полезной нагрузкой). Если же встраиваемая функция очень длинная, то размер бинарного файла увеличится, а это не ускоряет работу программы. Поэтому компилятор будет использовать ключевое слово inline как подсказку и может избавиться от вызовов функций, встраивая их тело. Он даже может встроить отдельные функции, которые программист не объявлял встраиваемыми.
Дополнительная информация
Одним из способов решения такой задачи до появления C++17 было создание функции static, которая возвращает ссылку на объект static:
class foo {
public: