Обнаружение во время компиляции или во время выполнения в функции 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() часть.

Другие вопросы по тегам