Metaprogramación en C++ y en D
metaprogramming d2 (10)
El mecanismo de plantilla en C ++ solo se volvió accidentalmente útil para la metaprogramación de plantillas. Por otro lado, D''s fue diseñado específicamente para facilitar esto. Y aparentemente es aún más fácil de entender (o eso he escuchado).
No tengo experiencia con D, pero tengo curiosidad, ¿qué es lo que se puede hacer en D y no se puede en C ++, cuando se trata de metaprogramación de plantillas?
Aquí hay un fragmento de código D que hace un map()
hecho a medida map()
que devuelve sus resultados por referencia .
Crea dos matrices de longitud 4, asigna cada par de elementos correspondiente al elemento con el valor mínimo, lo multiplica por 50 y almacena el resultado en la matriz original .
Algunas características importantes a tener en cuenta son las siguientes:
Las plantillas son variadas:
map()
podría tomar cualquier cantidad de argumentos.¡El código es (relativamente) corto ! La estructura de
Mapper
, que es la lógica central, tiene solo 15 líneas, y sin embargo puede hacer tanto con tan poco. Mi punto no es que esto sea imposible en C ++, pero ciertamente no es tan compacto y limpio.
import std.metastrings, std.typetuple, std.range, std.stdio;
void main() {
auto arr1 = [1, 10, 5, 6], arr2 = [3, 9, 80, 4];
foreach (ref m; map!min(arr1, arr2)[1 .. 3])
m *= 50;
writeln(arr1, arr2); // Voila! You get: [1, 10, 250, 6][3, 450, 80, 4]
}
auto ref min(T...)(ref T values) {
auto p = &values[0];
foreach (i, v; values)
if (v < *p)
p = &values[i];
return *p;
}
Mapper!(F, T) map(alias F, T...)(T args) { return Mapper!(F, T)(args); }
struct Mapper(alias F, T...) {
T src; // It''s a tuple!
@property bool empty() { return src[0].empty; }
@property auto ref front() {
immutable sources = FormatIota!(q{src[%s].front}, T.length);
return mixin(Format!(q{F(%s)}, sources));
}
void popFront() { foreach (i, x; src) { src[i].popFront(); } }
auto opSlice(size_t a, size_t b) {
immutable sliced = FormatIota!(q{src[%s][a .. b]}, T.length);
return mixin(Format!(q{map!F(%s)}, sliced));
}
}
// All this does is go through the numbers [0, len),
// and return string ''f'' formatted with each integer, all joined with commas
template FormatIota(string f, int len, int i = 0) {
static if (i + 1 < len)
enum FormatIota = Format!(f, i) ~ ", " ~ FormatIota!(f, len, i + 1);
else
enum FormatIota = Format!(f, i);
}
Bien en D, puede imponer fácilmente restricciones estáticas en los parámetros de la plantilla y escribir código dependiendo del argumento de la plantilla real con static si .
Es posible simular eso para casos simples con C ++ usando la especialización de plantillas y otros trucos (ver boost) pero es un PITA y causa muy limitada porque el compilador no expone muchos detalles sobre los tipos.
Una cosa que C ++ realmente no puede hacer es sofisticada generación de código de tiempo de compilación.
Creo que nada está mejor calificado para mostrar la increíble potencia (TM) del sistema de plantillas D que este renderizador que encontré hace años:
¡Sí! Esto es en realidad lo que genera el compilador ... es el "programa", y bastante colorido, de hecho.
Editar
La fuente parece estar de vuelta en línea.
En D puede verificar el tamaño de un tipo y los métodos disponibles en él y decidir qué implementación desea usar
esto se usa, por ejemplo, en el módulo core.atomic
bool cas(T,V1,V2)( shared(T)* here, const V1 ifThis, const V2 writeThis ){
static if(T.sizeof == byte.sizeof){
//do 1 byte CaS
}else static if(T.sizeof == short.sizeof){
//do 2 byte CaS
}else static if( T.sizeof == int.sizeof ){
//do 4 byte CaS
}else static if( T.sizeof == long.sizeof ){
//do 8 byte CaS
}else static assert(false);
}
Escribí mis experiencias con las plantillas de D''s, los mixins de cadenas y los mixins de plantilla: http://david.rothlis.net/d/templates/
Debería darle una idea de lo que es posible en D - No creo que en C ++ pueda acceder a un identificador como una cadena, transformar esa cadena en tiempo de compilación y generar código a partir de la cadena manipulada.
Mi conclusión: Extremadamente flexible, extremadamente poderosa y utilizable por simples mortales, pero el compilador de referencia todavía tiene errores cuando se trata de la metaprogramación en tiempo de compilación más avanzada.
Hay algunas cosas silenciosas que puede hacer en la metaprogramación de plantillas en D que no puede hacer en C ++. ¡Lo más importante es que puede hacer metaprogramación de plantillas SIN TANTO DE DOLOR!
Los mejores ejemplos de metaprogramación D son los módulos de biblioteca estándar D que hacen uso intensivo de él frente a los módulos C ++ Boost y STL. Consulte D''s std.range , std.algorithm , std.functional y std.parallelism . Ninguno de estos sería fácil de implementar en C ++, al menos con el tipo de API limpia y expresiva que tienen los módulos D.
La mejor manera de aprender la metaprogramación D, en mi humilde opinión, es mediante este tipo de ejemplos. Aprendí mucho leyendo el código en std.algorithm y std.range, que fueron escritos por Andrei Alexandrescu (un gurú de metaprogramación de plantillas de C ++ que se ha involucrado mucho con D). Luego utilicé lo que aprendí y contribuí con el módulo estándar de parálisis.
También tenga en cuenta que D tiene una función de compilación de evaluación (CTFE) que es similar al constexpr
C ++ 1x pero mucho más general en que un subconjunto grande y creciente de funciones que pueden evaluarse en tiempo de ejecución puede evaluarse sin modificaciones en tiempo de compilación. Esto es útil para la generación de código en tiempo de compilación, y el código generado se puede compilar utilizando string mixins .
Manipulación de cadenas, incluso el análisis de cadenas.
Esta es una biblioteca de MP que genera analizadores de voz recursivos basados en gramáticas definidas en cadenas que usan (más o menos) BNF. No lo he tocado en años, pero solía funcionar.
Solo para contrarrestar la publicación de seguimiento de rayos D, aquí hay un rastreador de rayos en tiempo de compilación de C ++ ( metatrace ):
(por cierto, usa sobre todo metaprogramación de C ++ 2003, sería más legible con los nuevos constexpr
)
Las dos cosas más importantes que ayudan a la metaprogramación de plantillas en D son las restricciones de plantilla y static if
- ambas de las cuales C ++ teóricamente podría agregar y que la beneficiarían enormemente.
Las restricciones de plantilla le permiten poner una condición en una plantilla que debe ser verdadera para que la plantilla pueda ser instanciada. Por ejemplo, esta es la firma de una de las sobrecargas de std.algorithm.find
:
R find(alias pred = "a == b", R, E)(R haystack, E needle)
if (isInputRange!R &&
is(typeof(binaryFun!pred(haystack.front, needle)) : bool))
Para que esta función de plantilla pueda ser instanciada, el tipo R
debe ser un rango de entrada definido por std.range.isInputRange
(por lo que isInputRange!R
debe ser true
), y el predicado dado debe ser una función binaria que compila con los argumentos dados y devuelve un tipo que es implícitamente convertible a bool
. Si el resultado de la condición en la restricción de la plantilla es false
, la plantilla no se compilará. Esto no solo lo protege de los desagradables errores de plantilla que obtiene en C ++ cuando las plantillas no se compilan con sus argumentos, sino que lo hace de modo que pueda sobrecargar las plantillas en función de sus restricciones de plantilla. Por ejemplo, hay otra sobrecarga de find
que es
R1 find(alias pred = "a == b", R1, R2)(R1 haystack, R2 needle)
if (isForwardRange!R1 && isForwardRange!R2
&& is(typeof(binaryFun!pred(haystack.front, needle.front)) : bool)
&& !isRandomAccessRange!R1)
Toma exactamente los mismos argumentos, pero su restricción es diferente. Por lo tanto, los diferentes tipos funcionan con diferentes sobrecargas de la misma función de plantilla, y la mejor implementación de find
se puede usar para cada tipo. No hay forma de hacer ese tipo de cosas limpiamente en C ++. Con un poco de familiaridad con las funciones y plantillas usadas en la restricción de plantilla típica, las restricciones de plantilla en D son bastante fáciles de leer, mientras que necesita una metaprogramación de plantillas muy complicada en C ++ para intentar algo como esto, que su programador promedio no es va a ser capaz de entender, y mucho menos hacer realidad por sí mismos. Boost es un excelente ejemplo de esto. Hace algunas cosas increíbles, pero es increíblemente complicado.
static if
mejora la situación aún más. Al igual que con las restricciones de plantilla, cualquier condición que se pueda evaluar en tiempo de compilación se puede usar con ella. p.ej
static if(isIntegral!T)
{
//...
}
else static if(isFloatingPoint!T)
{
//...
}
else static if(isSomeString!T)
{
//...
}
else static if(isDynamicArray!T)
{
//...
}
else
{
//...
}
En qué rama se compila depende de qué condición se evalúa primero como true
. Por lo tanto, dentro de una plantilla, puede especializar piezas de su implementación en función de los tipos con los que se creó la instancia de la plantilla, o en base a cualquier otra cosa que pueda evaluarse en tiempo de compilación. Por ejemplo, core.time
usa
static if(is(typeof(clock_gettime)))
para compilar el código de forma diferente en función de si el sistema proporciona clock_gettime
o no (si clock_gettime
está allí, lo usa, de lo contrario, usa gettimeofday
).
Probablemente el ejemplo más crudo que he visto donde D mejora las plantillas es con un problema que mi equipo de trabajo encontró en C ++. Necesitábamos crear una instancia de una plantilla de forma diferente en función de si el tipo que se le dio se derivaba de una clase base particular o no. Terminamos usando una solución basada en esta pregunta de desbordamiento de pila . Funciona, pero es bastante complicado simplemente comprobar si un tipo se deriva de otro.
En D, sin embargo, todo lo que tienes que hacer es usar el operador :
p.ej
auto func(T : U)(T val) {...}
Si T
es implícitamente convertible a U
(como lo sería si T
se derivara de U
), entonces func
compilará, mientras que si T
no es implícitamente convertible a U
, entonces no lo hará. Esa simple mejora hace que incluso las especializaciones básicas de plantillas sean mucho más potentes (incluso sin restricciones de plantillas o static if
).
Personalmente, rara vez uso plantillas en C ++ que no sean contenedores y la función ocasional en <algorithm>
, porque son muy difíciles de usar. Producen errores feos y son muy difíciles de hacer. Para hacer algo incluso un poco complicado, debe ser muy hábil con las plantillas y la metaprogramación de plantillas. Sin embargo, con plantillas en D, es tan fácil que las uso todo el tiempo. Los errores son mucho más fáciles de entender y manejar (aunque aún son peores que los errores típicos de las funciones sin plantillas), y no tengo que imaginar cómo forzar al lenguaje a hacer lo que quiero con metaprogramación sofisticada. .
No hay ninguna razón para que C ++ no pueda obtener gran parte de estas habilidades que tiene D (los conceptos de C ++ ayudarían si alguna vez los solucionan), pero hasta que agreguen compilación condicional básica con construcciones similares a las restricciones de plantilla y static if
a C ++, C ++ las plantillas simplemente no podrán compararse con las plantillas D en términos de facilidad de uso y potencia.