Подтвердить что ты не робот

Обнаружение времени компиляции или времени выполнения в функции constexpr

Я был взволнован, когда constexpr был введен в С++ 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
}

У меня есть серьезные сомнения в том, что MSVС++ имеет что-то подобное, но я надеялся, что, возможно, GCC или clang, по крайней мере, что-то достигнут, однако он не может выглядеть так.

Конечно, представленный мной пример был очень упрощенным, но если вы можете использовать свое воображение, есть много случаев, когда вы можете свободно делать что-то вроде широко используемого выражения ветвления внутри функции, которая, как вы знаете, может выполняться только при время компиляции, потому что, если оно выполняется во время выполнения, производительность будет скомпрометирована.

4b9b3361

Ответ 1

Можно определить, является ли данное выражение функции-выражения постоянным выражением и тем самым выбирать между двумя различными реализациями. Требуется С++ 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__) ) ? \
        Static_Struct :: 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__)

Ответ 2

Я понял, что это будет способ гарантировать, что MyMin можно использовать только с константами, зависящими от времени компиляции, и это обеспечит, что компилятор никогда не позволит его выполнение во время выполнения

Да; есть способ.

И работает также с С++ 11.

Google-ing Я нашел странный путь отравления (Скотт Шурр): одним словом, следующий

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 дает ошибку в фазе связывания.

В более общем плане есть предложение (когда-либо от Скотта Шурра), но я не знаю о реализации.

--- EDIT ---

Как указано T.C. (спасибо!) это решение работает (если работает и когда работает) только потому, что компилятор не оптимизирует в точке, чтобы понять, что 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) ;
 }

но не является реальным решением для функции, в которой не существует "естественного" случая, способного к выбросу.

Когда это естественный случай ошибки, который должен давать ошибку (компиляция или запуск), он отличается: компилятор не может оптимизировать и трюк работает.

Пример: if MyMin() возвращает значение minimun между 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().