void t "может реализовать концепции"?
Я смотрел вторую часть выступление Уолтера Брауна CppCon2014 по шаблонному метапрограммированию, во время которого он обсуждал использование своего романа void_t<>
строительство. Во время своего выступления Питер Соммерлад задал ему вопрос, который я не совсем понял. (ссылка идет непосредственно на вопрос, обсуждаемый код имел место непосредственно перед этим)
Sommerlad попросил
Уолтер, это означает, что мы действительно можем реализовать концепции lite прямо сейчас?
на что Уолтер ответил
О да! Я сделал это ... Это не совсем тот же синтаксис.
я понял, что этот обмен был о понятиях Lite. Действительно ли это шаблон это универсальный? По какой-то причине я этого не вижу. Может ли кто-нибудь объяснить (или нарисовать), как это может выглядеть? Это просто о enable_if
и определяющие черты, или каков был вопрошающий имеете в виду?
The void_t
шаблон определяется следующим образом:
template<class ...> using void_t = void;
он использует это затем, чтобы обнаружить, если операторы типа хорошо сформированы, используя это для реализации is_copy_assignable
тип признака:
//helper type
template<class T>
using copy_assignment_t
= decltype(declval<T&>() = declval<T const&>());
//base case template
template<class T, class=void>
struct is_copy_assignable : std::false_type {};
//SFINAE version only for types where copy_assignment_t<T> is well-formed.
template<class T>
struct is_copy_assignable<T, void_t<copy_assignment_t<T>>>
: std::is_same<copy_assignment_t<T>,T&> {};
из-за разговора я понимаю, как работает этот пример, но я не вижу, как мы отсюда перейдем к чему-то вроде Concepts Lite.
1 ответ:
да, concepts lite в основном одевается SFINAE. Кроме того, это позволяет глубже самоанализ, чтобы обеспечить лучшую перегрузку. Однако это работает только в том случае, если предикаты понятия определены как
concept bool
. Улучшенная перегрузка не работает с текущими предикатами концепта, но может использоваться условная перегрузка. Давайте посмотрим, как мы можем определять предикаты, ограничивать шаблоны и перегружать функции в C++14. Это довольно долго, но он идет над тем, как создать все инструменты, необходимые для достичь этого в C++14.Определение Предикатов
во-первых, это своего рода некрасиво читать предикат со всеми
std::declval
иdecltype
везде. Вместо этого мы можем воспользоваться тем фактом, что мы можем ограничить функцию, используя конечный decltype(из сообщения в блоге Эрика ниблера здесь), как это:struct Incrementable { template<class T> auto requires_(T&& x) -> decltype(++x); };
так что если
++x
не действует, тоrequires_
функция-член не вызывается. Так что мы можем создатьmodels
черта это просто проверяет, еслиrequires_
вызывается черезvoid_t
:template<class Concept, class Enable=void> struct models : std::false_type {}; template<class Concept, class... Ts> struct models<Concept(Ts...), void_t< decltype(std::declval<Concept>().requires_(std::declval<Ts>()...)) >> : std::true_type {};
Ограничивающие Шаблоны
поэтому, когда мы хотим ограничить шаблон, основанный на концепции, нам все равно нужно будет использовать
enable_if
, но мы можем использовать этот макрос, чтобы сделать его чище:#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0
таким образом, мы можем определить
increment
функция, которая ограничена на основе :template<class T, REQUIRES(models<Incrementable(T)>())> void increment(T& x) { ++x; }
так что если мы позвоним
increment
С чем-то неIncrementable
мы получить ошибку, как это:test.cpp:23:5: error: no matching function for call to 'incrementable' incrementable(f); ^~~~~~~~~~~~~ test.cpp:11:19: note: candidate template ignored: disabled by 'enable_if' [with T = foo] template<class T, REQUIRES(models<Incrementable(T)>())> ^
Перегрузка Функций
теперь, если мы хотим сделать перегрузку, мы хотим использовать условную перегрузку. Скажем, мы хотим создать
std::advance
используя предикаты концепта, мы могли бы определить его следующим образом(пока мы будем игнорировать декрементируемый случай):struct Incrementable { template<class T> auto requires_(T&& x) -> decltype(++x); }; struct Advanceable { template<class T, class I> auto requires_(T&& x, I&& i) -> decltype(x += i); }; template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())> void advance(Iterator& it, int n) { it += n; } template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())> void advance(Iterator& it, int n) { while (n--) ++it; }
однако это вызывает неоднозначную перегрузку(в concepts lite это все равно будет неоднозначная перегрузка, если мы не изменим наши предикаты для ссылки другие предикаты в
concept bool
) когда его используют сstd::vector
итератор. То, что мы хотим сделать, это упорядочить вызовы, которые мы можем сделать с помощью условной перегрузки. Можно подумать о написании чего-то вроде этого(что не является допустимым C++):template<class Iterator> void advance(Iterator& it, int n) if (models<Advanceable(Iterator, int)>()) { it += n; } else if (models<Incrementable(Iterator)>()) { while (n--) ++it; }
поэтому, если первая функция не вызывается, она вызовет следующую функцию. Итак, давайте начнем с реализации его для двух функций. Мы создадим класс под названием
basic_conditional
, который принимает два объекта функции в качестве шаблона параметры:struct Callable { template<class F, class... Ts> auto requires_(F&& f, Ts&&... xs) -> decltype( f(std::forward<Ts>(xs)...) ); }; template<class F1, class F2> struct basic_conditional { // We don't need to use a requires clause here because the trailing // `decltype` will constrain the template for us. template<class... Ts> auto operator()(Ts&&... xs) -> decltype(F1()(std::forward<Ts>(xs)...)) { return F1()(std::forward<Ts>(xs)...); } // Here we add a requires clause to make this function callable only if // `F1` is not callable. template<class... Ts, REQUIRES(!models<Callable(F1, Ts&&...)>())> auto operator()(Ts&&... xs) -> decltype(F2()(std::forward<Ts>(xs)...)) { return F2()(std::forward<Ts>(xs)...); } };
Итак, теперь это означает, что нам нужно определить наши функции как объекты функций:
struct advance_advanceable { template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())> void operator()(Iterator& it, int n) const { it += n; } }; struct advance_incrementable { template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())> void operator()(Iterator& it, int n) const { while (n--) ++it; } }; static conditional<advance_advanceable, advance_incrementable> advance = {};
Итак, если мы попытаемся использовать его с
std::vector
:std::vector<int> v = { 1, 2, 3, 4, 5, 6 }; auto iterator = v.begin(); advance(iterator, 4); std::cout << *iterator << std::endl;
он будет компилироваться и распечатать
,5
.std::advance
на самом деле имеет три перегрузки, так что мы можем использоватьbasic_conditional
для реализацииconditional
это работает для любого количества функций с использованием рекурсии:template<class F, class... Fs> struct conditional : basic_conditional<F, conditional<Fs...>> {}; template<class F> struct conditional<F> : F {};
Итак, теперь мы можем написать полный
std::advance
такой:struct Incrementable { template<class T> auto requires_(T&& x) -> decltype(++x); }; struct Decrementable { template<class T> auto requires_(T&& x) -> decltype(--x); }; struct Advanceable { template<class T, class I> auto requires_(T&& x, I&& i) -> decltype(x += i); }; struct advance_advanceable { template<class Iterator, REQUIRES(models<Advanceable(Iterator, int)>())> void operator()(Iterator& it, int n) const { it += n; } }; struct advance_decrementable { template<class Iterator, REQUIRES(models<Decrementable(Iterator)>())> void operator()(Iterator& it, int n) const { if (n > 0) while (n--) ++it; else { n *= -1; while (n--) --it; } } }; struct advance_incrementable { template<class Iterator, REQUIRES(models<Incrementable(Iterator)>())> void operator()(Iterator& it, int n) const { while (n--) ++it; } }; static conditional<advance_advanceable, advance_decrementable, advance_incrementable> advance = {};
Перегрузка Лямбдами
однако, кроме того, мы могли бы использовать лямбды для его записи вместо объектов функций, которые могут помочь сделать его более чистым для записи. Поэтому мы используем это
STATIC_LAMBDA
макрос для построения лямбд во время компиляции:struct wrapper_factor { template<class F> constexpr wrapper<F> operator += (F*) { return {}; } }; struct addr_add { template<class T> friend typename std::remove_reference<T>::type *operator+(addr_add, T &&t) { return &t; } }; #define STATIC_LAMBDA wrapper_factor() += true ? nullptr : addr_add() + []
и добавить