Почему я должен избегать std :: enable_if в сигнатурах функций

Скотт Мейерс опубликовал содержание и статус его следующей книги EC ++ 11.
Он написал, что один пункт в книге может быть «Избегайте std::enable_if в функции подписи «.

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

В этот вопрос все три решения показаны.

В качестве параметра функции:

template<typename T>
struct Check1
{
template<typename U = T>
U read(typename std::enable_if<
std::is_same<U, int>::value >::type* = 0) { return 42; }

template<typename U = T>
U read(typename std::enable_if<
std::is_same<U, double>::value >::type* = 0) { return 3.14; }
};

В качестве параметра шаблона:

template<typename T>
struct Check2
{
template<typename U = T, typename std::enable_if<
std::is_same<U, int>::value, int>::type = 0>
U read() { return 42; }

template<typename U = T, typename std::enable_if<
std::is_same<U, double>::value, int>::type = 0>
U read() { return 3.14; }
};

Как тип возврата:

template<typename T>
struct Check3
{
template<typename U = T>
typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
return 42;
}

template<typename U = T>
typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
return 3.14;
}
};
  • Какое решение должно быть предпочтительным и почему я должен избегать других?
  • В каких случаях «Избегайте std::enable_if в функции подписи « касается использования в качестве возвращаемого типа (который не является частью обычной сигнатуры функции, но специализации шаблона)?
  • Существуют ли различия для шаблонов функций-членов и не-членов?

155

Решение

Поместите взлом в параметры шаблона.

enable_if Подход к параметру шаблона имеет как минимум два преимущества перед остальными:

  • читабельность: использование enable_if и типы return / аргумент не объединяются в один беспорядочный кусок двусмысленных типов и обращений к вложенным типам; даже несмотря на то, что беспорядок неоднозначности и вложенного типа может быть уменьшен с помощью шаблонов псевдонимов, это все равно объединит две несвязанные вещи. Использование enable_if связано с параметрами шаблона, а не с типами возврата. Наличие их в параметрах шаблона означает, что они ближе к тому, что имеет значение;

  • универсальная применимостьконструкторы не имеют возвращаемых типов, а некоторые операторы не могут иметь дополнительных аргументов, поэтому ни один из двух других параметров не может быть применен везде. Помещение enable_if в параметр шаблона работает везде, так как в любом случае вы можете использовать только SFINAE для шаблонов.

Для меня аспект читабельности является большим мотивирующим фактором в этом выборе.

101

Другие решения

std::enable_if опирается наСбой Substition не является ошибкой«(иначе СФИНА) принцип во время вычет аргумента шаблона. Это очень хрупкий особенность языка, и вы должны быть очень осторожны, чтобы сделать это правильно.

  1. если ваше состояние внутри enable_if содержит вложенный шаблон или определение типа (подсказка: ищите :: токены), то разрешение этих вложенных храмов или типов обычно не выводимый контекст. Любой сбой замещения в таком невыбранном контексте является ошибка.
  2. различные условия в нескольких enable_if перегрузки не могут иметь никакого перекрытия, потому что разрешение перегрузки будет неоднозначным. Это то, что вы, как автор, должны проверить сами, хотя вы получите хорошие предупреждения компилятора.
  3. enable_if манипулирует набором жизнеспособных функций во время разрешения перегрузки, которые могут иметь неожиданные взаимодействия в зависимости от присутствия других функций, которые вводятся из других областей (например, через ADL). Это делает его не очень надежным.

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

template<typename T>
T fun(T arg)
{
return detail::fun(arg, typename some_template_trait<T>::type() );
}

namespace detail {
template<typename T>
fun(T arg, std::false_type /* dummy */) { }

template<typename T>
fun(T arg, std::true_type /* dummy */) {}
}

Диспетчеризация тегов не манипулирует набором перегрузки, но помогает вам выбрать именно ту функцию, которую вы хотите, предоставляя правильные аргументы через выражение времени компиляции (например, в признаке типа). По моему опыту, это намного легче отладить и получить права. Если вы начинающий писатель библиотеки со сложными чертами типа, вам может понадобиться enable_if как-то, но для большинства регулярных условий компиляции это не рекомендуется.

54

Какое решение должно быть предпочтительным и почему я должен избегать других?

  • Параметр шаблона

    • Это можно использовать в Конструкторах.
    • Это можно использовать в определяемом пользователем операторе преобразования.
    • Требуется C ++ 11 или более поздняя версия.
    • Это ИМО, тем более читабельно.
    • Это может легко использоваться неправильно и приводит к ошибкам с перегрузками:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()
      

    уведомление typename = std::enable_if_t<cond> вместо правильного std::enable_if_t<cond, int>::type = 0

  • тип возврата:

    • Его нельзя использовать в конструкторе. (нет возвращаемого типа)
    • Его нельзя использовать в определяемом пользователем операторе преобразования. (не вычитается)
    • Можно использовать pre-C ++ 11.
    • Второе более читаемое ИМО.
  • Последнее, в параметре функции:

    • Можно использовать pre-C ++ 11.
    • Это можно использовать в Конструкторах.
    • Его нельзя использовать в определяемом пользователем операторе преобразования. (без параметров)
    • Его нельзя использовать в методах с фиксированным числом аргументов (унарные / бинарные операторы +, -, *…)
    • Его можно смело использовать в наследстве (см. Ниже).
    • Изменить сигнатуру функции (в качестве последнего аргумента у вас есть дополнительный void* = nullptr) (поэтому указатель на функцию будет отличаться и т. д.)

Существуют ли различия для шаблонов функций-членов и не-членов?

Есть тонкие различия с наследованием и using:

Согласно using-declarator (выделение мое):

namespace.udecl

Набор объявлений, введенных декларатором using, определяется путем выполнения поиска подходящего имени ([basic.lookup.qual], [class.member.lookup]) для имени в деклараторе using, за исключением функций, которые скрыты, как описано ниже.

Когда использование-декларатор переносит объявления из базового класса в производный класс, функции-члены и шаблоны функций-членов в производном классе переопределяют и / или скрывают функции-члены и шаблоны функций-членов с тем же именем, параметром-тип-списком, cv-квалификацией и ref-квалификатором (если есть) в базовом классе (а не противоречивый). Такие скрытые или переопределенные объявления исключаются из набора объявлений, введенных с помощью-объявления.

Таким образом, как для аргумента шаблона, так и для возвращаемого типа методы скрыты в следующем сценарии:

struct Base
{
template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
void f() {}

template <std::size_t I>
std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
using Base::f; // Useless, f<0> is still hidden
using Base::g; // Useless, g<0> is still hidden

template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
void f() {}

template <std::size_t I>
std::enable_if_t<I == 1> g() {}
};

демонстрация (gcc неправильно находит базовую функцию).

В то время как с аргументом, аналогичный сценарий работает:

struct Base
{
template <std::size_t I>
void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
using Base::h; // Base::h<0> is visible

template <std::size_t I>
void h(std::enable_if_t<I == 1>* = nullptr) {}
};

демонстрация

4
По вопросам рекламы [email protected]