Обнаружение во время компиляции или во время выполнения в функции constexpr
Я был взволнован, когда constexpr был представлен в C++11, но я, к сожалению, сделал оптимистичные предположения о его полезности. Я предположил, что мы можем использовать constexpr где угодно, чтобы перехватить литеральные константы времени компиляции или любой результат constexpr литеральной константы времени компиляции, включая что-то вроде этого:
constexpr float MyMin(constexpr float a, constexpr float b) { return a<b?a:b; }
Поскольку определение типа возвращаемого значения функции только как constexpr не ограничивает ее использование во время компиляции, а также должно вызываться во время выполнения, я решил, что это будет способом гарантировать, что MyMin может быть когда-либо использован только с вычисленными во время компиляции константами, и это гарантирует, что компилятор никогда не допустит его выполнения во время выполнения, освобождая меня от написания альтернативной, более дружественной ко времени выполнения версии MyMin, в идеале с тем же именем, которое использует встроенную функцию _mm_min_ss, гарантируя, что компилятор не будет генерировать ветвление во время выполнения. код. К сожалению, параметры функции не могут быть constexpr, поэтому может показаться, что это невозможно сделать, если не возможно что-то подобное:
constexpr float MyMin(float a, float b)
{
#if __IS_COMPILE_TIME__
return a<b?a:b;
#else
return _mm_cvtss_f32(_mm_min_ss(_mm_set_ss(a),_mm_set_ss(b)));
#endif
}
У меня есть серьезные сомнения, что в MSVC++ есть что-то подобное, но я надеялся, что GCC или Clang найдут хоть что-то для этого, каким бы нелегким это не выглядело.
Конечно, пример, который я представил, был очень упрощенным, но если вы можете использовать свое воображение, во многих случаях вы можете свободно делать что-то вроде расширенного использования операторов ветвления внутри функции, которая, как вы знаете, может выполняться только во время компиляции, потому что если он выполняется во время выполнения, производительность будет поставлена под угрозу.
2 ответа
Можно определить, является ли данное выражение вызова функции постоянным выражением, и, таким образом, выбрать между двумя различными реализациями. Требуется C++14 для общей лямбды, используемой ниже.
(Этот ответ вырос из ответа @Yakk на вопрос, который я задавал в прошлом году).
Я не уверен, насколько далеко я продвигаю Стандарт. Это проверено на clang 3.9, но заставляет g++ 6.2 выдавать "внутреннюю ошибку компилятора". Я отправлю отчет об ошибке на следующей неделе (если никто не сделает это первым!)
Этот первый шаг должен переместить constexpr
реализация в struct
как constexpr static
метод. Проще говоря, вы можете оставить текущий constexpr
как есть и позвонить с constexpr static
метод нового struct
,
struct StaticStruct {
static constexpr float MyMin_constexpr (float a, float b) {
return a<b?a:b;
}
};
Кроме того, определите это (даже если это выглядит бесполезно!):
template<int>
using Void = void;
Основная идея заключается в том, что Void<i>
требует, чтобы i
быть постоянным выражением. Точнее, эта следующая лямбда будет иметь подходящие перегрузки только при определенных обстоятельствах:
auto l = [](auto ty)-> Void<(decltype(ty):: MyMin_constexpr(1,3) ,0)>{};
\------------------/
testing if this
expression is a
constant expression.
Мы можем позвонить l
только если аргумент ty
имеет тип StaticStruct
и если наше выражение интереса (MyMin_constexpr(1,3)
) является постоянным выражением. Если мы заменим 1
или же 3
с непостоянными аргументами, то общая лямбда l
потеряет метод через SFINAE.
Поэтому следующие два теста эквивалентны:
- Является
StaticStruct::MyMin_constexpr(1,3)
постоянное выражение?- Можно
l
быть вызванным черезl(StaticStruct{})
?
Заманчиво просто удалить auto ty
а также decltype(ty)
из вышеупомянутой лямбды. Но это даст серьезную ошибку (в непостоянном случае) вместо приятного сбоя замещения. Поэтому мы используем auto ty
чтобы получить ошибку замещения (которую мы можем обнаружить) вместо ошибки.
Следующий код довольно просто вернуть std:true_type
если и только если f
(наша общая лямбда) может быть вызвана с a
(StaticStruct
):
template<typename F,typename A>
constexpr
auto
is_a_constant_expression(F&& f, A&& a)
-> decltype( ( std::forward<F>(f)(std::forward<A>(a)) , std::true_type{} ) )
{ return {}; }
constexpr
std::false_type is_a_constant_expression(...)
{ return {}; }
Далее, демонстрация его использования:
int main() {
{
auto should_be_true = is_a_constant_expression(
[](auto ty)-> Void<(decltype(ty):: MyMin_constexpr(1,3) ,0)>{}
, StaticStruct{});
static_assert( should_be_true ,"");
}
{
float f = 3; // non-constexpr
auto should_be_false = is_a_constant_expression(
[](auto ty)-> Void<(decltype(ty):: MyMin_constexpr(1,f) ,0)>{}
, StaticStruct{});
static_assert(!should_be_false ,"");
}
}
Чтобы решить вашу исходную проблему напрямую, мы могли бы сначала определить макрос для сохранения повторения:
(Я не проверял этот макрос, извиняюсь за любые опечатки.)
#define IS_A_CONSTANT_EXPRESSION( EXPR ) \
is_a_constant_expression( \
[](auto ty)-> Void<(decltype(ty):: \
EXPR ,0)>{} \
, StaticStruct{})
На этом этапе, возможно, вы могли бы просто сделать:
#define MY_MIN(...) \
IS_A_CONSTANT_EXPRESSION( MyMin_constexpr(__VA_ARGS__) ) ? \
StaticStruct :: MyMin_constexpr( __VA_ARGS__ ) : \
MyMin_runtime ( __VA_ARGS__ )
или, если вы не доверяете своему компилятору оптимизировать std::true_type
а также std::false_type
через ?:
тогда, возможно:
constexpr
float MyMin(std::true_type, float a, float b) { // called if it is a constant expression
return StaticStruct:: MyMin_constexpr(a,b);
}
float MyMin(std::false_type, float , float ) { // called if NOT a constant expression
return MyMin_runtime(a,b);
}
с этим макросом вместо:
#define MY_MIN(...) \
MyMin( IS_A_CONSTANT_EXPRESSION(MyMin_constexpr(__VA_ARGS__)) \
, __VA_ARGS__)
Я подумал, что это был бы способ гарантировать, что MyMin может быть когда-либо использован только с константами, оцененными во время компиляции, и это гарантировало бы, что компилятор никогда не разрешит его выполнение во время выполнения
Да; есть выход.
И работает с C++11 тоже.
Я нашел странный способ отравления (Скотт Шурр): короче, следующее
extern int no_symbol;
constexpr float MyMin (float a, float b)
{
return a != a ? throw (no_symbol)
: (a < b ? a : b) ;
}
int main()
{
constexpr float m0 { MyMin(2.0f, 3.0f) }; // OK
float f1 { 2.0f };
float m1 { MyMin(f1, 3.0f) }; // linker error: undefined "no_symbol"
}
Если я хорошо понимаю, идея заключается в том, что если MyMin()
выполняется время компиляции, throw(no_symbol)
никогда не используется (a != a
всегда ложно), поэтому нет необходимости использовать no_symbol
это объявлено extern
но никогда не определяется (и throw()
не может быть использовано время компиляции).
Если вы используете MyMin()
время выполнения, throw(no_symbol)
компилируется и no_symbol
выдает ошибку в фазе компоновки.
В более общем плане, есть предложение (когда-либо от Скотта Шурра), но я не знаю о реализации.
--- РЕДАКТИРОВАТЬ ---
Как указывает TC (спасибо!), Это решение работает (если работает и когда работает) только потому, что компилятор не оптимизирует в момент, чтобы понять, что a != a
всегда ложно
Особенно, MyMin()
работает (без хороших оптимизаций), потому что, в примере, мы работаем с числами с плавающей точкой и a != a
может быть правдой, если a
является NaN, так что для компилятора сложнее обнаружить, что throw()
часть бесполезна. Если MyMin()
это функция для целых чисел, тело может быть написано (с тестом float(a) != float(a)
попытаться помешать оптимизации компилятора) как
constexpr int MyMin (int a, int b)
{
return float(a) != float(a) ? throw (no_symbol)
: (a < b ? a : b) ;
}
но не является реальным решением для функции, в которой не существует "естественного" случая ошибки с возможностью выброса.
Когда это естественный случай ошибки, который должен давать ошибку (компиляция или запуск), то все по-другому: компилятор не может оптимизировать, и хитрость работает.
Пример: если MyMin()
вернуть минимальное значение между a
а также b
но a
а также b
должны быть разными или MyMin()
должен дать ошибку компилятора (не хороший пример... я знаю), так
constexpr float MyMin (float a, float b)
{
return a != b ? throw (no_symbol)
: (a < b ? a : b) ;
}
работает, потому что компилятор не может оптимизировать a != b
и должен скомпилировать (с ошибкой компоновщика) throw()
часть.