Каков рекомендуемый способ моделирования концепций и ограничений?

Перед введением концепций и ограничений есть несколько способов смоделировать эту проверку во время компиляции. Возьмем, к примеру, функцию "order()": (как реализовать LessThanComparable без концепций и ограничений - это отдельная история)

  • Используйте static_assert

    template <typename T, typename U>
    void order(T& a, U& b)
    {
        static_assert(LessThanComparable<U,T>, "oh this is not epic");
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    Этот подход не работает при перегрузке функций.

  • Используйте typename = enable_if

    template <typename T, typename U,
        typename = std::enable_if_t<LessThanComparable<U,T>>>>
    void order(T& a, U& b)
    {
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    Что, если чрезмерно «умный» парень укажет третий параметр вручную?

  • Используйте enable_if в прототипе функции:

    template <typename T, typename U>
    std::enable_if_t<LessThanComparable<U,T>>, void> order(T& a, U& b)
    {
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    Иногда также не работает при перегрузке функций.

  • Используйте enable_if как тип фиктивного параметра шаблона, не являющегося типом

    template <typename T, typename U,
        std::enable_if_t<LessThanComparable<U,T>>, void*> = nullptr> // or int = 0
    void order(T& a, U& b)
    {
        if (b < a)
        {
            using std::swap;
            swap(a, b);
        }
    }
    

    Я видел это раньше и не могу придумать никаких недостатков.

  • И много других вариантов.

Какие из них предпочтительнее или рекомендуются? Какие преимущества и недостатки? Любая помощь приветствуется.


person L. F.    schedule 25.12.2018    source источник
comment
Обычный способ - использовать свойства типа.   -  person πάντα ῥεῖ    schedule 25.12.2018
comment
@ πάνταῥεῖ Вы имеете в виду обычный способ реализации LessThanComparable?   -  person L. F.    schedule 25.12.2018
comment
Нет, в общем. Признаки типа используются для проверки концепций и ограничений параметров шаблона.   -  person πάντα ῥεῖ    schedule 25.12.2018
comment
@ πάνταῥεῖ Да, они очень полезны в метапрограммировании ????   -  person L. F.    schedule 25.12.2018
comment
Связано: stackoverflow.com/questions/20181702/   -  person πάντα ῥεῖ    schedule 25.12.2018
comment
Есть небольшой недостаток в подходе к параметрам шаблона без типа. Если вы хотите отделить определение от объявления (например, для функции-члена шаблона), вам придется продублировать ограничение.   -  person r3mus n0x    schedule 25.12.2018
comment
Связано с почему-следует-я-избегать-stdenable-if- in-function-signatures с одним недостатком, перечисленным в моем ответе о предпочтительном подходе.   -  person Jarod42    schedule 26.12.2018


Ответы (3)


Это сложная тема, и ответить на ваш вопрос непросто.

Во всяком случае, некоторые наблюдения / предложения, без всякой претензии на исчерпывающий характер.

(1) путь static_assert()

static_assert(LessThanComparable<U,T>, "oh this is not epic");

- хорошее решение, если вам нужна функция, которая работает только с некоторыми типами и выдает ошибку (явную ошибку, потому что вы можете выбрать сообщение об ошибке) при вызове с неправильными типами.

Но обычно это неправильное решение, когда вам нужна альтернатива. Это не решение SFINAE. Таким образом, вызов функции с аргументами неправильного типа дает ошибку и не позволяет использовать другую функцию для подстановки.

(2) Вы правы насчет

typename = std::enable_if_t</* some test */>>

решение. Пользователь может вручную указать третий параметр. В шутку говорю, что это решение можно «угнать».

Но это не единственный недостаток этого решения.

Предположим, у вас есть две дополнительные foo() функции, которые необходимо включить / отключить через SFINAE; первый, когда тест верен, второй, когда тот же тест, если ложный.

Вы можете подумать, что следующее решение опасно (может быть взломано), но может работать

/* true case */
template <typename T, typename = std::enable_if_t<true == some_test_v<T>>>
void foo (T t)
 { /* something with t */ }

/* false case */
template <typename T, typename = std::enable_if_t<false == some_test_v<T>>>
void foo (T t)
 { /* something with t */ }

Неправильно: это решение просто не работает, потому что вы включаете / отключаете не второе имя типа, а только значение по умолчанию для второго имени типа. Таким образом, вы не полностью включаете / отключаете функции, и компилятор должен учитывать две функции с одинаковой сигнатурой (сигнатура функции не зависит от значений по умолчанию); значит, вы столкнулись с ошибкой.

Следующие решения, SFINAE по возвращаемому типу

std::enable_if_t<LessThanComparable<U,T>, void> order(T& a, U& b)

(также без void, это тип по умолчанию

std::enable_if_t<LessThanComparable<U,T>> order(T& a, U& b)

) или над вторым типом (следуя предложению Якка о нестандартном разрешенном void *)

template <typename T, typename U,
          std::enable_if_t<LessThanComparable<U,T>>, bool> = true> 

являются (ИМХО) хорошими решениями, потому что оба они избегают риска угона и совместимы с двумя дополнительными функциями с тем же именем и подписью.

Я предлагаю третье возможное решение (без возможности взлома, дополнительная совместимость), которое представляет собой добавление третьего значения по умолчанию с включенным / отключенным типом SFINAE: что-то вроде

template <typename T, typename U>
void order(T& a, U& b, std::enable_if_t<LessThanComparable<U,T>> * = nullptr)

Другое возможное решение - вообще избегать SFINAE, но использовать диспетчеризацию тегов; что-то как

template <typename T, typename U>
void order_helper (T & a, U & b, std::true_type const &)
 { if (b < a) { std::swap(a, b); } }

// something different if LessThanComparable is false ?
template <typename T, typename U>
void order_helper (T & a, U & b, std::false_type const &)
 { /* ???? */ }

template <typename T, typename U>
void order (T & a, U & b)
 { order_helper(a, b, LessThanComparable<U,T>{}); }

Это в случае, если LessThanComplarable наследуется от std::true_type, когда условие истинно, от std::false_type, когда условие ложно.

В противном случае, если LessThanComparable дает только логическое значение, вызов order_helper() может быть

order_helper(a, b, std::integral_constant<bool, LessThanComparable<U,T>>{});

(3) если вы можете использовать C ++ 17, существует if constexpr способ избежать большой перегрузки.

template <typename T, typename U>
void order(T& a, U& b)
{
   if constexpr ( LessThanComparable<U, T> )
    { 
      if ( b < a )
         std::swap(a, b);
    }
   else
    {
      // what else ?
    }
}
person max66    schedule 25.12.2018

Не типовые параметры шаблона типа void* не допускаются по крайней мере в некоторых версиях стандарта; Я бы использовал bool со значением =true.

В противном случае используйте это.

person Yakk - Adam Nevraumont    schedule 25.12.2018
comment
Спасибо, я не знал, что вы не можете использовать void* в качестве параметра шаблона, не являющегося типом. - person L. F.; 26.12.2018
comment
В любом случае, эта страница, похоже, предполагает, что указатель на объект или функция разрешена как параметр шаблона, не являющийся типом, в каждом стандарте от C ++ 11 до C ++ 20. Считается ли void* указателем на объект? - person L. F.; 23.01.2019
comment
@ l.f. Я бы задала здесь вопрос с пометкой языкового юриста или прочитала бы стандарт; Меня беспокоит то, что я сталкивался с тем, что в остальном разумные компиляторы отказывались от аргументов void ptr, которых легко избежать, поэтому я просто избегаю их. - person Yakk - Adam Nevraumont; 23.01.2019

Вам следует посмотреть, как range-v3 библиотека имитирует концепции https://github.com/ericniebler/range-v3/blob/master/include/range/v3/range_concepts.hpp

Существует также способ использовать псевдонимы шаблонов для достижения чего-то похожего на концепции Использование шаблонов псевдонимов для sfinae: позволяет ли это язык?

И вы пропустили decltype варианта в своем списке:

template <typename T, typename U>
auto order(T& a, U& b) -> decltype(void(b < a))
{
    if (b < a)
    {
        using std::swap;
        swap(a, b);
    }
}

template <typename T, typename U,
    typename = decltype(void(b < a))>
void order(T& a, U& b)
{
    if (b < a)
    {
        using std::swap;
        swap(a, b);
    }
}
person Nikita Kniazev    schedule 25.12.2018
comment
decltype - это в основном способ написания черт. Аналогичным образом std::void_t разрешит SFINAE по типу вместо логического, но на самом деле не меняет варианты OP. - person Jarod42; 26.12.2018
comment
Я думаю, что decltype лучше принадлежит реализации таких концептуальных симуляторов, как LessThanComparable. Тоже вариант. - person L. F.; 26.12.2018