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

165

Скотт Мейерс опубликовал содержание и статус своей следующей книги 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в сигнатурах функций» касается использования в качестве возвращаемого типа (который не является частью обычной сигнатуры функции, а специализацией шаблона)?
  • Существуют ли различия для шаблонов функций-членов и не-членов?
hansmaad
источник
Потому что перегрузка так же хороша, как правило. Во всяком случае, делегируйте реализацию, которая использует (специализированные) шаблоны классов.
13
Функции-члены отличаются тем, что набор перегрузок включает в себя перегрузки, объявленные после текущей перегрузки. Это особенно важно при выполнении отложенного возвращаемого типа с переменным числом аргументов (где тип возвращаемого значения должен быть выведен из другой перегрузки)
2013 г.
1
Ну, просто субъективно я должен сказать , что в то время как часто будучи весьма полезным мне не нравится std::enable_ifзагромождать мои функции подписи (особенно некрасиво дополнительный nullptrаргумент функции версии) , потому что она всегда выглядит так , как это, странное хак (для чего - то static ifмогуществе сделать намного более красивым и чистым), используя шаблонную чёрную магию, чтобы использовать интересную языковую функцию. Вот почему я предпочитаю диспетчеризацию тегов, когда это возможно (ну, у вас все еще есть дополнительные странные аргументы, но не в общедоступном интерфейсе, а также гораздо менее уродливые и загадочные ).
Кристиан Рау
2
Я хочу спросить, что делает =0в typename std::enable_if<std::is_same<U, int>::value, int>::type = 0достижении? Я не мог найти правильные ресурсы, чтобы понять это. Я знаю, что первая часть раньше =0имела тип члена, intесли Uи intтакой же. Большое спасибо!
astroboylrx
4
@astroboylrx Забавно, я собирался оставить комментарий, отметив это. По сути, это = 0 указывает на то, что это параметр шаблона по умолчанию, не тип . Это сделано так, потому что параметры шаблона по умолчанию не являются частью подписи, поэтому вы не можете их перегружать.
Нир Фридман

Ответы:

107

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

enable_ifОт параметра шаблона подход имеет как минимум два преимущества по сравнению с другими:

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

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

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

Р. Мартиньо Фернандес
источник
4
Использование здесьFUNCTION_REQUIRES макроса делает его более приятным для чтения, и он также работает в компиляторах C ++ 03, а также полагается на использование в возвращаемом типе. Кроме того, использование параметров шаблона функции вызывает проблемы с перегрузкой, поскольку теперь сигнатура функции не является уникальной, вызывая неоднозначные ошибки перегрузки. enable_ifenable_if
Пол Фульц II
3
Это старый вопрос, но для тех, кто все еще читает: решение проблемы, поднятой @Paul, заключается в использовании enable_ifсо стандартным параметром нетипового шаблона, который допускает перегрузку. Т.е. enable_if_t<condition, int> = 0вместо typename = enable_if_t<condition>.
Нир Фридман
обратная ссылка на почти статический if: web.archive.org/web/20150726012736/http://flamingdangerzone.com/…
davidbak
@ R.MartinhoFernandes flamingdangerzoneссылка в вашем комментарии, похоже, ведет на страницу установки шпионских программ. Я отметил это для модераторского внимания.
Ниспио
58

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

  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как-то, но для большинства регулярных условий компиляции это не рекомендуется.

TemplateRex
источник
22
Диспетчеризация тегов имеет один недостаток: если у вас есть какая-то особенность, которая обнаруживает присутствие функции, и эта функция реализована с использованием подхода диспетчеризации тегов, она всегда сообщает об этом элементе как о наличии и приводит к ошибке, а не к потенциальной ошибке замещения. , SFINAE - это, прежде всего, метод устранения перегрузок из наборов кандидатов, а диспетчеризация тегов - это метод выбора между двумя (или более) перегрузками. Есть некоторые совпадения в функциональности, но они не эквивалентны.
Р. Мартиньо Фернандес
@ R.MartinhoFernandes Вы можете привести короткий пример и проиллюстрировать, как enable_ifэто сделать правильно?
TemplateRex
1
@ R.MartinhoFernandes Я думаю, что отдельный ответ, объясняющий эти моменты, может повысить ценность ОП. :-) Кстати, написание таких черт is_f_able- это то, что я считаю задачей для авторов библиотек, которые, конечно, могут использовать SFINAE, когда это дает им преимущество, но для «обычных» пользователей и с учетом этой черты is_f_ableя думаю, что диспетчеризация тегов проще.
TemplateRex
1
@hansmaad Я опубликовал краткий ответ на ваш вопрос и вместо этого расскажу о проблеме «СФИНАЕ или не СФИНАЕ» в своем блоге (это немного не по теме в этом вопросе). Я имею в виду, как только у меня будет время закончить.
Р. Мартиньо Фернандес
8
СФИНАЕ "хрупкая"? Какой?
Гонки легкости на орбите
5

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

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

    • Это можно использовать в Конструкторах.
    • Он может использоваться в определяемом пользователем операторе преобразования.
    • Требуется 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

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

...

Когда декларатор использования переносит объявления из базового класса в производный класс, функции-члены и шаблоны функций-членов в производном классе переопределяют и / или скрывают функции-члены и шаблоны функций-членов с одинаковыми именами, параметр-тип-список, 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) {}
};

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

Jarod42
источник