void_t “puede implementar conceptos”?

Estaba viendo la segunda parte de la charla CppCon2014 de Walter Brown sobre metaprogtwigció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 syntax.

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 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 using copy_assignment_t = decltype(declval() = declval()); //base case template template struct is_copy_assignable : std::false_type {}; //SFINAE version only for types where copy_assignment_t is well-formed. template struct is_copy_assignable<T, void_t<copy_assignment_t>> : std::is_same<copy_assignment_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 aquí ), así:

 struct Incrementable { template 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 struct models : std::false_type {}; template struct models().requires_(std::declval()...)) >> : 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())> 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())> ^ 

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 auto requires_(T&& x) -> decltype(++x); }; struct Advanceable { template auto requires_(T&& x, I&& i) -> decltype(x += i); }; template())> void advance(Iterator& it, int n) { it += n; } template())> 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 void advance(Iterator& it, int n) if (models()) { it += n; } else if (models()) { 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 auto requires_(F&& f, Ts&&... xs) -> decltype( f(std::forward(xs)...) ); }; template struct basic_conditional { // We don't need to use a requires clause here because the trailing // `decltype` will constrain the template for us. template auto operator()(Ts&&... xs) -> decltype(F1()(std::forward(xs)...)) { return F1()(std::forward(xs)...); } // Here we add a requires clause to make this function callable only if // `F1` is not callable. template())> auto operator()(Ts&&... xs) -> decltype(F2()(std::forward(xs)...)) { return F2()(std::forward(xs)...); } }; 

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

 struct advance_advanceable { template())> void operator()(Iterator& it, int n) const { it += n; } }; struct advance_incrementable { template())> void operator()(Iterator& it, int n) const { while (n--) ++it; } }; static conditional advance = {}; 

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

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

Comstackrá 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 struct conditional : basic_conditional> {}; template struct conditional : F {}; 

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

 struct Incrementable { template auto requires_(T&& x) -> decltype(++x); }; struct Decrementable { template auto requires_(T&& x) -> decltype(--x); }; struct Advanceable { template auto requires_(T&& x, I&& i) -> decltype(x += i); }; struct advance_advanceable { template())> void operator()(Iterator& it, int n) const { it += n; } }; struct advance_decrementable { template())> void operator()(Iterator& it, int n) const { if (n > 0) while (n--) ++it; else { n *= -1; while (n--) --it; } } }; struct advance_incrementable { template())> void operator()(Iterator& it, int n) const { while (n--) ++it; } }; static conditional 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 comstackción:

 struct wrapper_factor { template constexpr wrapper operator += (F*) { return {}; } }; struct addr_add { template friend typename std::remove_reference::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 constexpr conditional 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())) { it += n; }, STATIC_LAMBDA(auto& it, int n, REQUIRES(models())) { if (n > 0) while (n--) ++it; else { n *= -1; while (n--) --it; } }, STATIC_LAMBDA(auto& it, int n, REQUIRES(models())) { 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 constexpr auto modeled(Ts&&...) { return models(); } constexpr const advance = make_conditional( STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled(it, n))) { it += n; }, STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled(it))) { if (n > 0) while (n--) ++it; else { n *= -1; while (n--) --it; } }, STATIC_LAMBDA(auto& it, int n, REQUIRES(modeled(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.