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

'if' с параметрами шаблона или SFINAE является предпочтительным?

Предпочтительным является следующее:

template<typename T>
bool isNotZero(const T &a)
{
    if (std::is_floating_point<T>::value) return abs(a) > std::numeric_limits<T>::epsilon();
    else return a;
}

Или это:?

template<typename T>
std::enable_if<std::is_floating_point<T>::value, bool>::type
isNotZero(const T &a) { return abs(a) > std::numeric_limits<T>::epsilon(); }

template<typename T>
std::enable_if<std::is_integral<T>::value, bool>::type
isNotZero(const T &a) { return a; }

Обычно я использую первый тип, чтобы избежать многих версий функции.

Я считаю, что это точно то же самое.

Первая версия оптимизирована на этапе кода операции и вторая версия в стадии создания шаблона.

4b9b3361

Ответ 1

Я считаю, что это точно то же самое.

Я бы не сказал, что это точно то же самое.

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

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

Во втором случае вы компилируете (и выполняете, конечно) то, что подходит для типа ввода. На мой взгляд, это делает второй подход выше.

Теперь, хотя в этой конкретной ситуации, вероятно, не будет никакого различия, какой подход вы выберете, условные исполнения, решаемые условиями компиляции, должны быть выражены с помощью конструкций времени компиляции - SFINAE и перегрузки шаблонов, тогда как if следует использовать для условий, зависящих от времени выполнения системы.

Первый подход невозможен, например, если две ветки условного кода содержат код, который компилируется только при выполнении соответствующей ветки. Рассмотрим эти два типа:

struct X
{
    X(int) { }
};

struct Y
{
    Y() { }
};

И следующий шаблон функции:

template<typename T>
T foo(const T &a)
{
    if (std::is_constructible<T, int>::value)
    {
        return T(42);
    }
    else
    {
        return T();
    }
}

Теперь ни один из следующих вызовов не будет законным:

foo(X()); // ERROR! X is not default-constructible
foo(Y()); // ERROR! Y is not constructible from an int

Это само по себе говорит о том, что в целом соответствующий инструмент для обработки условного выполнения компиляции - это перегрузка шаблона + SFINAE (или эквивалентные конструкции, которые могут включать специализированные классы).

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

Все было бы иначе, если бы что-то вроде static if существовало на С++, но на данный момент это не так - и даже в ближайшем будущем, похоже.

Ответ 2

В настоящее время я предпочитаю использовать SFINAE. Использование SFINAE не требует какой-либо оптимизации, потому что вы явно разрешаете только одну из функций вызывать в зависимости от ситуации. Нет никакой оптимизации для выполнения просто потому, что соответствующая функция будет вызываться во время выполнения без принятия решения.

Использование условного оператора if ставит бремя на программу для принятия решения во время выполнения. Конечно, это можно было бы оптимизировать, но может и не быть. Вероятно, это приведет к компиляции обеих ветвей, независимо от того, какой из них фактически выполняется для конкретного аргумента шаблона. Это означает, что код в каждой ветки должен быть синтаксически и семантически корректным для любого заданного аргумента шаблона.

Возможно, в один прекрасный день у нас будет static if, что позволит вам написать оператор if как условие времени компиляции, но есть некоторые сильные чувства об этом на данный момент:

Функция static if, недавно предложенная для С++, и его принятие было бы катастрофой для языка.

Однако в ближайшем будущем (с целью выпуска около С++ 14) у нас могут быть ограничения (aka concept-lite), который позволит вам написать такие функции:

template<Floating_point T>
bool isNotZero(const T &a) { return abs(a) > std::numeric_limits<T>::epsilon(); }

template<Integral T>
bool isNotZero(const T &a) { return a; }

Что эквивалентно написанию:

template<typename T>
  requires Floating_point<T>()
bool isNotZero(const T &a) { return abs(a) > std::numeric_limits<T>::epsilon(); }

template<typename T>
  requires Integral<T>()
bool isNotZero(const T &a) { return a; }

Ограничения Floating_point и Integral - это просто предикаты constexpr для аргументов их шаблонов, которые проверяются во время компиляции и участвуют в разрешении перегрузки. Это будет предпочтительный способ записи такого набора функций.