Concepts Lite

Generic programming

template<typename Duck>
void talk(const Duck& duck)
{
    duck.quack();
}

talk(10); ???

Dado un tipo cualquiera T...

  • ¿Soporta T las operaciones que necesito?
  • ¿Implementa T las semánticas que necesito?
template<typename T>
T twice(const T& value)
{
    return value * 2;
}

Y si...

  • ¿Pudieramos comprobar qué operaciones soporta un tipo?
  • ¿Pudieramos especificar cuales de esas operaciones debe soportar un parámetro de plantilla?

SFINAE

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

template<typename T>
struct twiceable : 
    std::false_type 
{};

template<typename T>
struct twiceable<T, void_t<decltype(std::declval<T>() * 2)>> :
    std::true_type 
{};

SFINAE


template<typename T, 
         typename = typename std::enable_if<
             twiceable<T>::value
         >::type>
T twice(const T& value)
{
    return value * 2;
}


int i = twice(2);

SFINAE

  • Tricky
  • Nada intuitivo
  • Errores de compilación ininteligibles

Conceptos

Concepto

  • Conjunto de propiedades sintácticas y semánticas que caracterizan a un conjunto de tipos
  • Concepts Lite: Sintaxis para declarar un concepto en base a propiedades sintácticas

Function concept

template<typename T>
concept bool Type()
{
    return true;
}
  • Predicado lógico sobre uno o mas tipos
  • concept keyword
  • Implícitamente constexpr inline

Variable concept

template<typename T>
concept bool Type = true;
  • El predicado se representa mediante una plantilla de variable (C++14)

Expresión requires

template<typename T>
concept bool A = requires(T a)
{
    ...
};

template<typename T, typename U>
concept bool A = requires(T a, const U& b)
{
    ...
};

template<typename... Ts>
concept bool C = requires(Ts... ts)
{
    ...
};

"Sandbox" donde pedir valores con los que poder trabajar. Sustituye a std::declval<T>()

Requisitos

template<typename A, typename B = A>
concept bool Addable = requires(const A& a, const B& b)
{
    a + b;              // a + b es válido 
                        // (existe operator+(A,B))
    {a + b} -> int;     // a + b es válido 
                        // y devuelve int
    {a + b} -> Addable; // a + b es válido
                        // y devuelve tipo que satisface Addable
};
  • Requisitos simples
  • Requisitos compuestos

Restricciones

template<typename T>
concept bool Iterable = requires()
{
    typename T::iterator;                    // T tiene tipo miembro "iterator"
    typename std::iterator_traits<T>;        // Instancia de std::iterator_traits es válida
    requires Iterator<typename T::iterator>; // T::iterator satisface el concepto "Iterator"
};
  • Requisitos de tipo
  • Requisitos anidados

Usando conceptos

Como tipo de variables


template<typename Map, typename Key>
auto search(const Map& map, const Key& key)
{
    // it es de un tipo que satisface "Iterator"
    Iterator it = map.find(key); 

    ...
}
  • Como un auto "más concreto"

Cláusula requires


template<typename R, typename F>
    requires Iterable<R> && Invokable<F, typename R::value_type>
void foreach(const R& range, F function)
{
    for(const auto& e : range)
        function(e);
}
  • Permite establecer qué conceptos deben cumplir los parámetros de plantilla

Cláusula requires

template<typename A, typename B>
    requires requires(A a, B b) { a + b; }
auto add(const A& a, const B& b)
{
    return a + b;
}

  • In-place requires expression: Podemos definir constraints en el mismo sitio donde los usamos.

Plantillas abreviadas

template<typename T>
concept bool StreamableOut = requires(std::ostream& os, const T& value)
{
    { os << value } -> std::ostream&;
}

namespace boost
{
    std::string lexical_cast(const StreamableOut& value)
    {
        std::ostringstream os;

        os << value;

        return os.str();
    }
}
  • Usamos el nombre del concepto a satisfacer como tipo del parámetro
  • Ojo! Sigue siendo una plantilla

Plantillas abreviadas

template<typename T>
concept bool DefaultConstructible = std::is_default_constructible<T>::value;

template<typename T, typename Alloc>
concept bool Allocator = is_instance_of<Alloc, T>::value && 
requires(const Alloc& alloc)
{
    alloc.allocate(1);
    ...
}

template<DefaultConstructible T, Allocator<T> Alloc>
class vector
{
    ...
};
  • Por supuesto con plantillas de clase también funciona

Introducción a plantilla

template<typename F, typename... Args>
concept bool Invokable = requires(F f, Args... args)
{
    f(args...);
}


Invokable{F, Args...}
auto invoke(F f, Args&&... args)
{
    return f(std::forward<Args>(args)...);
}
  • Más azúcar: Concepto C de n parámetros T1, ..., Tn

Ejemplo: Iteradores

Iterator

template<typename It>
concept bool Iterator = requires(It lvalue)
{
    requires CopyConstructible<It>;
    requires CopyAssignable<It>;
    requires Destructible<It>;

    requires Swappable<It>;

    *lvalue;
    {++lvalue} -> It&
};

ForwardIterator

template<typename It>
concept bool ForwardIterator = requires(It i)
{
    requires InputIterator<It>;
    requires DefaultConstructible<It>;

    requires crab::Switch()
        .Case(OutputIterator<It, detail::value_type<It>>)
           .Then(Same<detail::value_type<It>&, detail::reference<It>>)
        .Default()
           .Then(Same<const detail::value_type<It>&, detail::reference<It>>)
    ();

    {i++} -> It;
    {*i++} -> detail::reference<It>;
};

Probando Concepts Lite

  • Implementación experimental en GCC
  • https://github.com/Manu343726/gcc_concepts_bugs
  • Imagen de docker con GCC trunk ya compilado
  • Ejemplos
  • Integración contínua

¿Preguntas?

Concepts Lite

By Manu Sánchez

Concepts Lite

  • 1,116