Void_t "может реализовать концепции"?

Я смотрел вторую часть Уолтер Браун CppCon2014 беседует о метапрограммировании шаблонов, в ходе которого он обсудил использование своей романной конструкции void_t<>, Во время своего выступления Питер Соммерлад задал ему вопрос, который я не совсем понял. (ссылка идет непосредственно на вопрос, обсуждаемый код проходил непосредственно перед этим)

Соммерлад спросил

Уолтер, это означало бы, что мы действительно можем реализовать концепции lite прямо сейчас?

на который ответил Уолтер

О да! Я сделал это... У него совсем не такой синтаксис.

Я понял, что этот обмен относится к 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

Да, концепции lite в основном одевают SFINAE. Плюс это позволяет более глубокую интроспекцию, чтобы обеспечить лучшую перегрузку. Однако это работает только в том случае, если предикаты понятия определены как concept bool. Улучшенная перегрузка не работает с текущими предикатами концепции, но может быть использована условная перегрузка. Давайте посмотрим, как мы можем определять предикаты, ограничивать шаблоны и функции перегрузки в С++ 14. Это довольно долго, но в нем рассказывается, как создать все инструменты, необходимые для выполнения этого в С++ 14.

Определение предикатов

Во-первых, довольно уродливо читать предикат со всеми std::declval и decltype всюду. Вместо этого мы можем воспользоваться тем фактом, что мы можем ограничить функцию, используя трейлинг-тип (из сообщения Eric Nieblers blog здесь), вот так:

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. Мы хотим сделать заказ, который мы можем сделать, используя условную перегрузку. Можно подумать о написании чего-то вроде этого (что недопустимо С++):

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

Перегрузка с помощью Lambdas

Однако, кроме того, мы могли бы использовать lambdas для записи вместо объектов функций, которые могут помочь сделать его более чистым для записи. Поэтому мы используем макрос STATIC_LAMBDA для создания lambdas во время компиляции:

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 может обрабатывать функции и перегружать.