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

Скотт Мейерс опубликовал содержание и статус своей следующей книги EС++ 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 в сигнатурах функций" касается использования в качестве возвращаемого типа (который не является частью обычной сигнатуры функции, а специализацией шаблона)?
  • Существуют ли какие-либо различия для шаблонов функций-членов и не-членов?

Ответ 1

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

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

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

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

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

Ответ 2

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

  • если ваше условие внутри enable_if содержит вложенный шаблон или определение типа (подсказка: искать текс ::), то разрешение этих вложенных шаблонов или типов обычно представляет собой невыводимый контекст. Любой сбой замены в таком не выводимом контексте является ошибкой.
  • различные условия в нескольких перегрузках enable_if не могут иметь перекрытия, поскольку разрешение перегрузки будет неоднозначным. Это то, что вам, как автору, нужно проверить сами, хотя вы получите хорошие предупреждения о компиляторе.
  • 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 как-то, но для большинства регулярных использования условий компиляции это не рекомендуется.

Ответ 3

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

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

    • Это можно использовать в Конструкторах.
    • Это можно использовать в определяемом пользователем операторе преобразования.
    • Требуется С++ 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-С++ 11.
    • Второе более читаемое ИМО.
  • Последнее, в параметре функции:

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

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

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

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

namespace.udecl

Набор объявлений, введенных декларатором using, определяется путем выполнения поиска подходящего имени ([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) {}
};

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