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

3 ответов


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

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

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

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

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


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

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

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

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

есть ли различия для членов и нечленов шаблоны функций?

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

по словам using-declarator (выделено мной):

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

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

...

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

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

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

демо