void_t "может реализовать концепции"?
Я смотрел вторую часть выступления Уолтера Брауна на CppCon2014 по шаблонному метапрограммированию, во время которого он обсуждал использование своего романа void_t<>
строительство. Во время своей презентации Питер Соммерлад задал ему вопрос, который я не совсем понял. (ссылка идет прямо на вопрос, обсуждаемый код был размещен непосредственно перед этим)
Соммерлад спросил
Уолтер, значит ли это, что мы действительно можем реализовать концепции прямо сейчас?
на что Уолтер ответил
О да! Я сделал это... Он не имеет такой же синтаксис.
Я понял, что этот обмен связан с Concepts Lite. Эта модель действительно настолько универсальна? По какой-то причине я этого не вижу. Может кто-нибудь объяснить (или набросать), как это может выглядеть? Это только о enable_if
и определение черт, или что имел в виду спрашивающий?
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 ответ
Да, концепт лайт в основном одевает СФИНА. Плюс это позволяет более глубокому самоанализу, чтобы учесть лучшую перегрузку. Однако это работает, только если предикаты концепции определены как concept bool
, Улучшенная перегрузка не работает с предикатами текущей концепции, но можно использовать условную перегрузку. Давайте посмотрим, как мы можем определять предикаты, шаблоны ограничений и функции перегрузки в C++14. Это довольно долго, но речь идет о том, как создать все инструменты, необходимые для достижения этой цели в C++14.
Определение предикатов
Во-первых, некрасиво читать предикат со всеми std::declval
а также decltype
везде. Вместо этого мы можем воспользоваться тем фактом, что мы можем ограничить функцию, используя конечный тип decl (из сообщения Эрика Ниблера здесь), например:
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
функция, которая ограничена на основе Incrementable
концепция:
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;
}
Однако это вызывает неоднозначную перегрузку (в концептах 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() + []
И добавить make_conditional
функция, которая constexpr
:
template<class... Fs>
constexpr conditional<Fs...> make_conditional(Fs...)
{
return {};
}
Тогда мы можем теперь написать advance
функционировать так:
constexpr const advance = make_conditional(
STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Advanceable(decltype(it), int)>()))
{
it += n;
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Decrementable(decltype(it))>()))
{
if (n > 0) while (n--) ++it;
else
{
n *= -1;
while (n--) --it;
}
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(models<Incrementable(decltype(it))>()))
{
while (n--) ++it;
}
);
Который немного более компактен и удобочитаем, чем использование версий объекта функции.
Кроме того, мы могли бы определить modeled
функция, чтобы уменьшить decltype
уродство:
template<class Concept, class... Ts>
constexpr auto modeled(Ts&&...)
{
return models<Concept(Ts...)>();
}
constexpr const advance = make_conditional(
STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Advanceable>(it, n)))
{
it += n;
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Decrementable>(it)))
{
if (n > 0) while (n--) ++it;
else
{
n *= -1;
while (n--) --it;
}
},
STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled<Incrementable>(it)))
{
while (n--) ++it;
}
);
Наконец, если вы заинтересованы в использовании существующих библиотечных решений (вместо того, чтобы использовать свои собственные, как я показал). Существует библиотека Tick, которая обеспечивает основу для определения концепций и шаблонов ограничений. И библиотека Fit может обрабатывать функции и перегрузки.