TL; DR
Antes de tentar ler esta postagem inteira, saiba que:
- uma solução para o problema apresentado foi encontrada por mim , mas ainda estou ansioso para saber se a análise está correta;
- Empacotei a solução em uma
fameta::counter
classe que resolve algumas peculiaridades restantes. Você pode encontrá-lo no github ; - você pode vê-lo trabalhando no godbolt .
Como tudo começou
Desde que Filip Roséen descobriu / inventou, em 2015, a magia negra que compila contadores de tempo em C ++ , fiquei levemente obcecado com o dispositivo, então, quando o CWG decidiu que a funcionalidade tinha que ir, fiquei desapontado, mas ainda esperançoso que sua mente pode ser alterado mostrando alguns casos de uso convincentes.
Então, alguns anos atrás, decidi dar uma olhada na coisa novamente, para que os uberswitch es pudessem ser aninhados - um caso de uso interessante, na minha opinião - apenas para descobrir que não funcionaria mais com as novas versões do os compiladores disponíveis, mesmo que o problema 2118 estivesse (e ainda esteja ) em estado aberto: o código seria compilado, mas o contador não aumentaria.
O problema foi relatado no site da Roséen e, recentemente, também no stackoverflow: O C ++ suporta contadores em tempo de compilação?
Alguns dias atrás, decidi tentar resolver os problemas novamente
Eu queria entender o que havia mudado nos compiladores que faziam o C ++, aparentemente ainda válido, não funcionar mais. Para esse fim, procurei em toda a web por alguém ter falado sobre isso, mas sem sucesso. Então, comecei a experimentar e cheguei a algumas conclusões, que estou apresentando aqui na esperança de obter um feedback dos mais bem informados do que eu por aqui.
Abaixo, apresento o código original de Roséen por uma questão de clareza. Para uma explicação de como funciona, consulte o site dele :
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");
}
Com os compiladores g ++ e clang ++ ish recente, next()
sempre retorna 1. Depois de experimentar um pouco, o problema pelo menos com o g ++ parece ser que, uma vez que o compilador avalia os parâmetros padrão dos modelos de funções na primeira vez em que as funções são chamadas, qualquer chamada subsequente para essas funções não acionam uma reavaliação dos parâmetros padrão, nunca instanciando novas funções, mas sempre se referindo às instanciadas anteriormente.
Primeiras perguntas
- Você realmente concorda com esse meu diagnóstico?
- Se sim, esse novo comportamento é obrigatório pelo padrão? O anterior foi um bug?
- Caso contrário, qual é o problema?
Tendo em mente o que foi next()
dito acima, criei uma solução alternativa : marque cada invocação com um ID único monotonicamente crescente, para passar para as callees, para que nenhuma chamada seja a mesma, forçando o compilador a reavaliar todos os argumentos cada vez.
Parece um fardo fazer isso, mas pensando nisso, pode-se usar as macros padrão __LINE__
ou __COUNTER__
semelhantes (sempre que disponíveis), ocultas em uma counter_next()
macro semelhante à função.
Então, eu vim com o seguinte, que apresento da forma mais simplificada que mostra o problema sobre o qual falarei mais adiante.
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>();
Você pode observar os resultados acima no godbolt , que eu capturei para os preguiçosos.
E como você pode ver, com o tronco g ++ e clang ++ até 7.0.0, funciona! , o contador aumenta de 0 para 3 conforme o esperado, mas com a versão clang ++ acima da 7.0.0, isso não acontece .
Para adicionar insulto à lesão, consegui travar o clang ++ até a versão 7.0.0, simplesmente adicionando um parâmetro "context" ao mix, para que o contador esteja realmente vinculado a esse contexto e, como tal, possa ser reiniciado sempre que um novo contexto for definido, o que abre a possibilidade de usar uma quantidade potencialmente infinita de contadores. Com essa variante, o clang ++ acima da versão 7.0.0 não falha, mas ainda não produz o resultado esperado. Viva em godbolt .
Perdendo qualquer pista sobre o que estava acontecendo, descobri o site cppinsights.io , que permite ver como e quando os modelos são instanciados. Usando esse serviço, o que eu acho que está acontecendo é que o clang ++ na verdade não define nenhuma das friend constexpr auto counter(slot<N>)
funções sempre que writer<N, I>
é instanciada.
Tentar chamar explicitamente counter(slot<N>)
qualquer N que já deveria ter sido instanciado parece fundamentar essa hipótese.
No entanto, se eu tentar explicitamente instanciar writer<N, I>
para qualquer dado N
e I
que já deveria ter sido instanciado, em seguida, clang ++ reclama uma redefinido friend constexpr auto counter(slot<N>)
.
Para testar o exposto, adicionei mais duas linhas ao código-fonte anterior.
int test1 = counter(slot<11>());
int test2 = writer<11,0>::value;
Você pode ver tudo por si mesmo no godbolt . Captura de tela abaixo.
Então, parece que o clang ++ acredita que definiu algo que acredita não ter definido , o que meio que faz sua cabeça girar, não é?
Segundo lote de perguntas
- Minha solução alternativa é C ++, ou consegui descobrir outro bug do g ++?
- Se for legal, descobri alguns erros desagradáveis do clang ++?
- Ou acabei de me aprofundar no submundo sombrio do comportamento indefinido, então eu mesmo sou o único culpado?
De qualquer forma, eu daria boas-vindas a qualquer um que quisesse me ajudar a sair dessa toca de coelho, dispensando explicações de dor de cabeça, se necessário. : D
next()
função, no entanto, eu realmente não consigo descobrir como isso funciona. Em qualquer caso, eu vim com uma resposta para o meu próprio problema, aqui: stackoverflow.com/a/60096865/566849Respostas:
Após uma investigação mais aprofundada, verifica-se que existe uma pequena modificação que pode ser executada na
next()
função, que faz o código funcionar corretamente nas versões clang ++ acima da 7.0.0, mas faz com que pare de funcionar para todas as outras versões clang ++.Veja o código a seguir, retirado da minha solução anterior.
Se você prestar atenção, o que ele literalmente faz é tentar ler o valor associado
slot<N>
, adicionar 1 a ele e associar esse novo valor ao mesmoslot<N>
.Quando
slot<N>
não tem valor associado, o valor associadoslot<Y>
é recuperado,Y
sendo o índice mais alto menor do queN
aquele queslot<Y>
possui um valor associado.O problema com o código acima é que, mesmo que funcione no g ++, o clang ++ (com razão, eu diria?) Faz retornar
reader(0, slot<N>())
permanentemente o que retornou quandoslot<N>
não tinha valor associado. Por sua vez, isso significa que todos os slots são efetivamente associados ao valor base0
.A solução é transformar o código acima neste:
Observe que
slot<N>()
foi modificado emslot<N-1>()
. Faz sentido: se eu quiser associar um valorslot<N>
, significa que nenhum valor está associado ainda; portanto, não faz sentido tentar recuperá-lo. Além disso, queremos aumentar um contador e o valor do contador associadoslot<N>
deve ser um mais o valor associadoslot<N-1>
.Eureka!
Porém, isso quebra as versões ++ do clang <= 7.0.0.
Conclusões
Parece-me que a solução original que publiquei possui um bug conceitual, de modo que:
Resumindo, o código a seguir funciona em todas as versões do g ++ e clang ++.
O código como está também funciona com o msvc. O compilador TPI não aciona SFINAE quando se usa
decltype(counter(slot<N>()))
, preferindo se queixam de não ser capaz dededuce the return type of function "counter(slot<N>)"
causait has not been defined
. Eu acredito que isso é um bug , que pode ser contornado com SFINAE no resultado direto decounter(slot<N>)
. Isso funciona com todos os outros compiladores também, mas o g ++ decide cuspir uma quantidade abundante de avisos muito irritantes que não podem ser desativados. Então, também neste caso,#ifdef
poderia vir ao resgate.A prova está em godbolt , screnshotted abaixo.
fonte