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

Тому що зазвичай занадто приємно перевантажувати. Якщо що, делегуйте програму, яка використовує (спеціалізовані) шаблони класів.
sehe

Функції члена відрізняються тим, що набір перевантажень включає перевантаження, оголошені після поточного перевантаження. Це особливо важливо, коли ви робите різні варіанти затримки повернення (де тип повернення слід зробити з іншого перевантаження)
1313

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параметра on template має щонайменше дві переваги перед іншими:

  • читабельність : типи використання enable_if та повернення / аргументи не об'єднуються в один брудний фрагмент розбірливих імен типу та вкладених типів доступу; незважаючи на те, що безлад та тип вкладення можна пом'якшити за допомогою шаблонів псевдонімів, це все одно об'єднає дві непоєднані речі разом. Використання 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>.
Нір Фрідман

зворотне посилання на майже-static-if: web.archive.org/web/20150726012736/http://flamingdangerzone.com/…
davidbak

@ R.MartinhoFernandes flamingdangerzoneпосилання у вашому коментарі, схоже, призводить до сторінки, яка встановлює шпигунське програмне забезпечення зараз. Я позначив це для уваги модератора.
nispio

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


22
Диспетчеризація тегів має і один недолік: якщо у вас є якась ознака, яка виявляє наявність функції, і ця функція реалізована з підходом диспетчеризації тегів, він завжди повідомляє цього члена як присутній і призводить до помилки замість потенційного збою заміни . SFINAE - це в першу чергу техніка для видалення перевантажень з кандидатських наборів, а диспетчеризація тегів - це техніка вибору між двома (або більше) перевантаженнями. Існує деяке перекриття у функціональності, але вони не рівноцінні.
Р. Мартіньо Фернандес

@ R.MartinhoFernandes Ви можете навести короткий приклад та проілюструвати, як enable_ifби це було правильно?
TemplateRex

1
@ R.MartinhoFernandes Я думаю, що окрема відповідь, що пояснює ці моменти, може додати значення ОП. :-) BTW, як писати такі риси - is_f_ableце те, що я вважаю завданням для письменників бібліотеки, які, звичайно, можуть використовувати SFINAE, коли це дає їм перевагу, але для "звичайних" користувачів та ознак is_f_able, я думаю, що диспетчеризація тегів простіше.
TemplateRex

1
@hansmaad Я опублікував коротку відповідь, що стосується вашого питання, і натомість надішле питання "SFINAE або не SFINAE" в публікації в блозі (це трохи не тематично в цьому питанні). Щойно я отримаю час закінчити це, я маю на увазі.
Р. Мартіньо Фернандес

8
SFINAE "тендітна"? Що?
Гонки легкості на орбіті

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

  • тип повернення:

    • Його не можна використовувати в конструкторі. (немає типу повернення)
    • Його не можна використовувати у визначеному користувачем операторі перетворення. (не виводиться)
    • Можна використовувати попередній C ++ 11.
    • Другий більш читаний ІМО.
  • Останній у параметрі функції:

    • Можна використовувати попередній C ++ 11.
    • Він може бути використаний у конструкторах.
    • Його не можна використовувати у визначеному користувачем операторі перетворення. (немає параметрів)
    • Вона не може бути використана в способах з фіксованим числом аргументів (одинарними / бінарними операторами +, -,* , ...)
    • Його можна сміливо використовувати у спадок (див. Нижче).
    • Змінення підпису функції (у вас в основному є додатковий як останній аргумент void* = nullptr) (так, покажчик функції відрізнявся б тощо)

Чи існують відмінності у шаблонах функцій членів та інших членів?

Існують тонкі відмінності у спадок та using :

Відповідно до using-declarator(моє наголос):

namespace.udecl

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

...

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

Отже, і для аргументу шаблону, і для типу повернення методи приховано наступним сценарієм:

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

Демо

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.