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


Scott Meyers posted содержание и статус из его следующей книги 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 в функции подписи" касается использования в качестве типа возврата (который не является частью обычной сигнатуры функции, но специализации шаблона)?
  • есть ли какие-либо различия для шаблоны функций-членов и не членов?
3 152

3 ответа:

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

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

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

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

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

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

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

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

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

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

пространство имен.udecl

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

...

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

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

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() {}
};

Demo (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) {}
};

Demo