Почему необходимо помечать функции как constexpr?
Проблема с использованием constexpr
в C++
C11 позволяет использовать функции, объявленные с спецификатором constexpr
, в константных выражениях, таких как аргументы шаблонов. Однако, есть строгие требования к тому, что может быть constexpr
, по сути, такая функция может инкапсулировать лишь одно подвыражение и ничего больше. (Примечание: это было ослаблено в C14, но суть вопроса остается).
В чем смысл требования к ключевому слову constexpr
? Что оно дает?
Хотя constexpr
действительно помогает выявить намерения интерфейса, оно не гарантирует, что функция будет использоваться в константных выражениях. После написания функции constexpr
программист по-прежнему должен:
- Написать тестовый случай или иным образом убедиться, что функция действительно используется в константном выражении.
- Документировать, какие значения параметров допустимы в контексте константных выражений.
Противоположно пониманию намерений, декорирование функций с помощью constexpr
может создать ложное чувство безопасности, поскольку проверяются лишь побочные синтаксические ограничения, игнорируя основное семантическое ограничение.
Вкратце: Потеряет ли язык что-то, если constexpr
в объявлениях функций сделать необязательным? Или окажет ли это какое-либо влияние на допустимые программы?
2 ответ(ов)
Предотвращение использования клиентским кодом значений, которые вы не гарантируете
Предположим, я пишу библиотеку и у меня есть функция, которая в данный момент возвращает константу:
awesome_lib.hpp
:
inline int f() { return 4; }
Если бы constexpr
не требовался, то вы, как автор клиентского кода, могли бы написать что-то вроде этого:
client_app.cpp
:
#include <awesome_lib.hpp>
#include <array>
std::array<int, f()> my_array; // требуется статический аргумент шаблона
int my_c_array[f()]; // требуется размер статического массива
Если я изменю f()
так, чтобы она возвращала значение из конфигурационного файла, ваш клиентский код сломается, и я не буду знать о том, что подверг вас риску поломки. На самом деле, возможно, вы обнаружите эту дополнительную проблему только тогда, когда у вас возникнут проблемы в производственной среде, и вам необходимо будет перекомпилировать код.
Изменив только реализацию f()
, я фактически изменил возможности использования интерфейса.
К счастью, начиная с C11, существует constexpr
, который позволяет мне пометить функцию так, чтобы клиентский код имел разумные ожидания, что функция останется constexpr
и может использоваться как такая. Я осведомлён о таком использовании и подтверждаю его как часть своего интерфейса. Подобно тому, как в C03 компилятор гарантирует, что клиентский код не будет зависеть от других не constexpr
функций, чтобы предотвратить вышеупомянутый сценарий "нежелательной/неизвестной зависимости"; это больше, чем просто документация – это принудительное соблюдение на этапе компиляции.
Стоит отметить, что это продолжает тенденцию C++ предлагать лучшие альтернативы для традиционного использования макросов препроцессора (например, рассмотрите #define F 4
, и как программист клиентского кода знает, считает ли программист библиотеки допустимым изменение на #define F config["f"]
), с их известными "злым" свойствами, такими как нахождение за пределами пространства имен/классов языка.
Почему нет диагностики для "очевидно" не константных функций?
Я думаю, путаница здесь связана с тем, что constexpr
не гарантирует, что существует какой-либо набор аргументов, для которых результат действительно будет константным во время компиляции: скорее, это требует от программиста ответственности за это (иначе §7.1.5/5 в Стандарте считает программу неверной, но не требует от компилятора выдачи диагностического сообщения). Да, это, безусловно, неудобно, но это не отменяет упомянутую выше полезность constexpr
.
Поэтому может быть полезно переключиться с вопроса "в чем смысл constexpr
" на "почему я могу скомпилировать функцию constexpr
, которая никогда не может вернуть значение во время компиляции?".
Ответ: потому что для этого потребовалось бы исчерпывающее анализирование ветвлений, которое могло бы затрагивать любое количество комбинаций. Это могло бы потребовать чрезмерного времени компиляции и/или памяти — даже превысить возможности любого вообразимого аппаратного обеспечения — для диагностики. Более того, даже когда это практично, необходимость точно диагностицировать такие случаи является совершенно новой проблемой для разработчиков компиляторов (у которых есть более полезные способы использовать свое время). Также это повлияло бы на программу, так как необходимо, чтобы определение функций, вызываемых из функции constexpr
, было доступно в момент выполнения валидации (и функции, вызовы и т. д.).
Тем временем, отсутствие constexpr
по-прежнему запрещает использование как значения, фиксируемого на этапе компиляции: строгость относится к стороне без constexpr
. Это полезно, как показано выше.
Сравнение с неконстантными членами
constexpr
предотвращаетint x[f()]
, в то время как отсутствиеconst
предотвращаетconst X x; x.f();
— оба препятствуют появлению нежелательных зависимостей/использования в клиентском кодев обоих случаях вы не хотите, чтобы компилятор автоматически определял
const[expr]
:- вы не хотите, чтобы клиентский код вызывал метод члена на
const
объекте, когда вы уже ожидаете, что эта функция будет развиваться, изменяя наблюдаемое значение и ломая клиентский код - вы не хотите, чтобы значение использовалось в качестве параметра шаблона или размера массива, если вы уже ожидаете, что оно будет определено во время выполнения
- вы не хотите, чтобы клиентский код вызывал метод члена на
они различаются тем, что компилятор принудительно использует
const
для других членов в членеconst
функции, но не обеспечивает компиляцию результата, равного константе, с помощьюconstexpr
(из-за практических ограничений компилятора и, возможно, чтобы одно определение функции могло с легкостью возвращать результаты во время выполнения для аргументов/значений, известных во время выполнения, но возвращало значение во время компиляции, когда это возможно и когда это требуется клиентским кодом).
Без ключевого слова компилятор не сможет выявить ошибки. Он не сможет сообщить вам, что функция является недопустимой с точки зрения синтаксиса как constexpr
. Хотя вы упомянули, что это создает "ложное чувство безопасности", я считаю, что лучше выявлять такие ошибки как можно раньше.
В чем разница между constexpr и const?
Когда действительно стоит использовать noexcept?
Возможно ли вывести тип переменной в стандартном C++?
Почему `std::initializer_list` не поддерживает оператор подиндексации?
Стоит ли игнорировать предупреждение "-Wmissing-braces" от gcc/clang?