TL; DR
Antes de intentar leer esta publicación completa, sepa que:
- Yo mismo he encontrado una solución al problema presentado , pero todavía estoy ansioso por saber si el análisis es correcto;
- He empaquetado la solución en una
fameta::counter
clase que resuelve algunas peculiaridades restantes. Puedes encontrarlo en github ; - Puedes verlo trabajando en Godbolt .
Cómo empezó todo
Desde que Filip Roséen descubrió / inventó, en 2015, la magia negra que compila los contadores de tiempo están en C ++ , he estado ligeramente obsesionado con el dispositivo, por lo que cuando el CWG decidió que la funcionalidad tenía que desaparecer, me decepcionó, pero todavía tenía la esperanza de que su mente podría cambiarse mostrándoles algunos casos de uso convincentes.
A continuación, un par de años decidí echar un vistazo a lo nuevo, por lo que uberswitch ES podrían ser anidados - un caso de uso interesante, en mi opinión - sólo para descubrir que no funcionaría por más tiempo con las nuevas versiones de los compiladores disponibles, aunque el problema 2118 estaba (y todavía está ) en estado abierto: el código se compilaría, pero el contador no aumentaría.
El problema se ha informado en el sitio web de Roséen y recientemente también en stackoverflow: ¿C ++ admite contadores de tiempo de compilación?
Hace unos días decidí intentar abordar los problemas nuevamente
Quería entender qué había cambiado en los compiladores que hicieron que el C ++, aparentemente válido, no funcionara más. Con ese fin, busqué por toda la red a alguien para hablar de ello, pero fue en vano. Así que comencé a experimentar y llegué a algunas conclusiones, que presento aquí con la esperanza de recibir comentarios de los más conocedores que yo.
A continuación, presento el código original de Roséen para mayor claridad. Para obtener una explicación de cómo funciona, consulte su sitio web :
template<int N>
struct flag {
friend constexpr int adl_flag (flag<N>);
};
template<int N>
struct writer {
friend constexpr int adl_flag (flag<N>) {
return N;
}
static constexpr int value = N;
};
template<int N, int = adl_flag (flag<N> {})>
int constexpr reader (int, flag<N>) {
return N;
}
template<int N>
int constexpr reader (float, flag<N>, int R = reader (0, flag<N-1> {})) {
return R;
}
int constexpr reader (float, flag<0>) {
return 0;
}
template<int N = 1>
int constexpr next (int R = writer<reader (0, flag<32> {}) + N>::value) {
return R;
}
int main () {
constexpr int a = next ();
constexpr int b = next ();
constexpr int c = next ();
static_assert (a == 1 && b == a+1 && c == b+1, "try again");
}
Con los compiladores g ++ recientes y clang ++, next()
siempre devuelve 1. Habiendo experimentado un poco, el problema al menos con g ++ parece ser que una vez que el compilador evalúa los parámetros predeterminados de las plantillas de funciones la primera vez que se llaman las funciones, cualquier llamada posterior a esas funciones no desencadenan una reevaluación de los parámetros predeterminados, por lo tanto, nunca crean instancias de nuevas funciones sino que siempre se refieren a las instancias previamente instanciadas.
Primeras preguntas
- ¿Estás de acuerdo con este diagnóstico mío?
- En caso afirmativo, ¿es este nuevo comportamiento exigido por la norma? ¿Fue el anterior un error?
- Si no, ¿cuál es el problema?
Teniendo en cuenta lo anterior, se me ocurrió una next()
solución : marque cada invocación con una identificación única que aumente monotónicamente, para pasar a las calles, de modo que ninguna llamada sea la misma, lo que obliga al compilador a reevaluar todos los argumentos cada vez.
Parece una carga hacer eso, pero al pensar en eso, uno podría usar las macros estándar __LINE__
o __COUNTER__
similares (siempre que estén disponibles), ocultas en una counter_next()
macro similar a una función.
Así que se me ocurrió lo siguiente, que presento en la forma más simplificada que muestra el problema del que hablaré más adelante.
template <int N>
struct slot;
template <int N>
struct slot {
friend constexpr auto counter(slot<N>);
};
template <>
struct slot<0> {
friend constexpr auto counter(slot<0>) {
return 0;
}
};
template <int N, int I>
struct writer {
friend constexpr auto counter(slot<N>) {
return I;
}
static constexpr int value = I-1;
};
template <int N, typename = decltype(counter(slot<N>()))>
constexpr int reader(int, slot<N>, int R = counter(slot<N>())) {
return R;
};
template <int N>
constexpr int reader(float, slot<N>, int R = reader(0, slot<N-1>())) {
return R;
};
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
return R;
}
int a = next<11>();
int b = next<34>();
int c = next<57>();
int d = next<80>();
Puede observar los resultados de lo anterior en godbolt , que he capturado para los perezosos.
Y como puede ver, con trunk g ++ y clang ++ hasta 7.0.0 ¡funciona! , el contador aumenta de 0 a 3 como se esperaba, pero con la versión de clang ++ por encima de 7.0.0 no lo hace .
Para agregar un insulto a la lesión, he logrado hacer que Clang ++ hasta la versión 7.0.0 se bloquee, simplemente agregando un parámetro de "contexto" a la mezcla, de modo que el contador esté realmente vinculado a ese contexto y, como tal, pueda se reiniciará cada vez que se defina un nuevo contexto, lo que abre la posibilidad de usar una cantidad potencialmente infinita de contadores. Con esta variante, clang ++ anterior a la versión 7.0.0 no se bloquea, pero aún así no produce el resultado esperado. Vivir en Godbolt .
Ante la pérdida de alguna pista sobre lo que estaba sucediendo, descubrí el sitio web cppinsights.io , que permite ver cómo y cuándo se instancian las plantillas. Usando ese servicio, lo que creo que está sucediendo es que clang ++ en realidad no define ninguna de las friend constexpr auto counter(slot<N>)
funciones cada vez que writer<N, I>
se instancia.
Intentar invocar explícitamente counter(slot<N>)
cualquier N dado que ya debería haberse instanciado parece dar base a esta hipótesis.
Sin embargo, si trato de crear una instancia explícita writer<N, I>
para cualquier cosa N
y I
eso ya debería haberse creado, entonces clang ++ se queja de una redefinición friend constexpr auto counter(slot<N>)
.
Para probar lo anterior, agregué dos líneas más al código fuente anterior.
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Puedes verlo todo por ti mismo en Godbolt . Captura de pantalla a continuación.
Entonces, parece que clang ++ cree que ha definido algo que cree que no ha definido , lo que hace que la cabeza gire, ¿no?
Segundo lote de preguntas
- ¿Es mi solución legal C ++ o me las arreglé para descubrir otro error de g ++?
- Si es legal, ¿descubrí algunos errores desagradables de clang ++?
- ¿O acabo de adentrarme en el oscuro inframundo de Comportamiento indefinido, para que yo mismo sea el único culpable?
En cualquier caso, daría una calurosa bienvenida a cualquiera que quisiera ayudarme a salir de esta madriguera de conejos, dispensando explicaciones dolorosas si fuera necesario. :RE
next()
función, sin embargo, no puedo entender cómo funciona eso. En cualquier caso, he encontrado una respuesta a mi propio problema, aquí: stackoverflow.com/a/60096865/566849Respuestas:
Después de una investigación adicional, resulta que existe una modificación menor que se puede realizar a la
next()
función, lo que hace que el código funcione correctamente en las versiones de clang ++ anteriores a 7.0.0, pero hace que deje de funcionar para todas las demás versiones de clang ++.Eche un vistazo al siguiente código, tomado de mi solución anterior.
Si le prestas atención, lo que literalmente hace es intentar leer el valor asociado
slot<N>
, agregarle 1 y luego asociar este nuevo valor al mismoslot<N>
.Cuando
slot<N>
no tiene ningún valor asociado,slot<Y>
se recupera el valor asociado con ,Y
siendo el índice más alto menor que elN
queslot<Y>
tiene un valor asociado.El problema con el código anterior es que, a pesar de que funciona en g ++, clang ++ (¿con razón, diría?) Hace que devuelva
reader(0, slot<N>())
permanentemente lo que devolvió cuandoslot<N>
no tenía un valor asociado. A su vez, esto significa que todas las ranuras se asocian efectivamente con el valor base0
.La solución es transformar el código anterior en este:
Tenga en
slot<N>()
cuenta que se ha modificado enslot<N-1>()
. Tiene sentido: si quiero asociar un valorslot<N>
, significa que todavía no hay ningún valor asociado, por lo tanto, no tiene sentido intentar recuperarlo. Además, queremos aumentar un contador, y el valor del contador asociadoslot<N>
tiene que ser uno más el valor asociadoslot<N-1>
.Eureka!
Sin embargo, esto rompe las versiones de clang ++ <= 7.0.0.
Conclusiones
Me parece que la solución original que publiqué tiene un error conceptual, tal que:
Resumiendo todo eso, el siguiente código funciona en todas las versiones de g ++ y clang ++.
El código tal como está también funciona con msvc. El compilador icc no activa SFINAE cuando se usa
decltype(counter(slot<N>()))
, prefiriendo quejarse porque no puede hacerlodeduce the return type of function "counter(slot<N>)"
porque síit has not been defined
. Creo que esto es un error , que se puede solucionar haciendo SFINAE en el resultado directo decounter(slot<N>)
. Esto también funciona en todos los otros compiladores, pero g ++ decide escupir una gran cantidad de advertencias muy molestas que no se pueden desactivar. Entonces, también en este caso,#ifdef
podría venir al rescate.La prueba está en Godbolt , fotografiado a continuación.
fuente