c++ - void_t "puede implementar conceptos"?



templates c++11 (1)

Estaba viendo la segunda parte de la charla CppCon2014 de Walter Brown sobre metaprogramación de plantillas , durante la cual discutió los usos de su novedosa construcción void_t<> . Durante su presentación, Peter Sommerlad le hizo una pregunta que yo no entendí del todo. (el enlace va directamente a la pregunta, el código en discusión se realizó directamente antes de eso)

Sommerlad preguntó

Walter, ¿eso significaría que realmente podemos implementar conceptos lite en este momento?

a lo que Walter respondió

¡Oh si! Lo he hecho ... No tiene la misma sintaxis.

Entendí que este intercambio era sobre Conceptos Lite. ¿Es este patrón realmente tan versátil? Por alguna razón, no lo estoy viendo. ¿Alguien puede explicar (o bosquejar) cómo se vería algo así? ¿Esto es solo sobre enable_if y definir rasgos, o a qué se estaba refiriendo el interrogador?

La plantilla void_t se define de la siguiente manera:

template<class ...> using void_t = void;

Él usa esto para detectar si las declaraciones de tipo están bien formadas, usando esto para implementar el rasgo de tipo 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&> {};

Debido a la charla, entiendo cómo funciona este ejemplo, pero no veo cómo llegar a algo así como Concepts Lite.


Sí, concept lite básicamente viste SFINAE. Además, permite una introspección más profunda para permitir una mejor sobrecarga. Sin embargo, eso solo funciona si los predicados conceptuales se definen como concept bool . La sobrecarga mejorada no funciona con los predicados conceptuales actuales, pero se puede usar la sobrecarga condicional. Veamos cómo podemos definir predicados, restringir plantillas y sobrecargar funciones en C ++ 14. Esto es algo largo, pero repasa cómo crear todas las herramientas necesarias para lograr esto en C ++ 14.

Definición de Predicados

Primero, es algo feo leer el predicado con todos los std::declval y decltype todas partes. En su lugar, podemos aprovechar el hecho de que podemos restringir una función usando un declotipo posterior (desde el blog de Eric Niebler here ), así:

struct Incrementable
{
    template<class T>
    auto requires_(T&& x) -> decltype(++x);
};

Entonces, si ++x no es válido, entonces la función de miembro require_ no es invocable. De modo que podemos crear un rasgo de models que solo verifica si se requires_ void_t usando 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
{};

Restricción de plantillas

Entonces, cuando queremos restringir la plantilla en función del concepto, aún necesitaremos usar enable_if , pero podemos usar esta macro para ayudar a que sea más enable_if :

#define REQUIRES(...) typename std::enable_if<(__VA_ARGS__), int>::type = 0

De modo que podemos definir una función de increment que está limitada en función del concepto Incrementable :

template<class T, REQUIRES(models<Incrementable(T)>())>
void increment(T& x)
{
    ++x;
}

Entonces, si llamamos al increment con algo que no es Incrementable , obtendremos un error como este:

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)>())>
                  ^

Funciones de sobrecarga

Ahora, si queremos hacer una sobrecarga, queremos usar una sobrecarga condicional. Digamos que queremos crear un std::advance usando predicados conceptuales, podríamos definirlo así (por ahora ignoraremos el caso decrementable):

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

Sin embargo, esto provoca una sobrecarga ambigua (En conceptos lite, esto aún sería una sobrecarga ambigua a menos que cambiemos nuestros predicados para referirnos a los otros predicados en un concept bool ) cuando se usa con std::vector iterator. Lo que queremos hacer es ordenar las llamadas, lo que podemos hacer utilizando la sobrecarga condicional. Se puede pensar en escribir algo como esto (que no es válido en 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;
}

Entonces, si no se llama a la primera función, llamará a la siguiente función. Así que comencemos por implementarlo para dos funciones. Crearemos una clase llamada basic_conditional que acepta dos objetos de función como parámetros de plantilla:

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

Entonces eso significa que necesitamos definir nuestras funciones como objetos de funciones en su lugar:

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

Entonces, si tratamos de usarlo con std::vector :

std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
auto iterator = v.begin();
advance(iterator, 4);
std::cout << *iterator << std::endl;

Compilará e imprimirá 5 .

Sin embargo, std::advance realidad tiene tres sobrecargas, por lo que podemos usar el basic_conditional para implementar un conditional que funcione para cualquier cantidad de funciones usando recursion:

template<class F, class... Fs>
struct conditional : basic_conditional<F, conditional<Fs...>>
{};

template<class F>
struct conditional<F> : F
{};

Entonces, ahora podemos escribir el std::advance completo como este:

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

Sobrecarga con Lambdas

Sin embargo, adicionalmente, podríamos usar lambdas para escribirlo en lugar de objetos de función que pueden ayudar a que sea más fácil escribir. Entonces usamos esta macro STATIC_LAMBDA para construir lambdas en tiempo de compilación:

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() + []

Y agrega una función make_conditional que es constexpr :

template<class... Fs>
constexpr conditional<Fs...> make_conditional(Fs...)
{
    return {};
}

Entonces ahora podemos escribir la función de advance esta manera:

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

Que es un poco más compacto y legible que usar las versiones del objeto de función.

Además, podríamos definir una función modeled para reducir la fealdad de 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;
    }
);

Finalmente, si está interesado en usar las soluciones de biblioteca existentes (en lugar de hacer las suyas propias como he demostrado). Existe la biblioteca Tick que proporciona un marco para definir conceptos y plantillas restrictivas. Y la biblioteca Fit puede manejar las funciones y la sobrecarga.





c++-concepts