Contadores de tempo de compilação C ++, revisitados

28

TL; DR

Antes de tentar ler esta postagem inteira, saiba que:

  1. uma solução para o problema apresentado foi encontrada por mim , mas ainda estou ansioso para saber se a análise está correta;
  2. Empacotei a solução em uma fameta::counterclasse que resolve algumas peculiaridades restantes. Você pode encontrá-lo no github ;
  3. 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

  1. Você realmente concorda com esse meu diagnóstico?
  2. Se sim, esse novo comportamento é obrigatório pelo padrão? O anterior foi um bug?
  3. 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.

insira a descrição da imagem aqui

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 Ne Ique 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.

clang ++ acredita que definiu algo que acredita que não definiu

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

  1. Minha solução alternativa é C ++, ou consegui descobrir outro bug do g ++?
  2. Se for legal, descobri alguns erros desagradáveis ​​do clang ++?
  3. 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

Fabio A.
fonte
2
Relacionados: stackoverflow.com/questions/51601439/...
HolyBlackCat
2
Como me lembro do comitê padrão, as pessoas têm a intenção clara de proibir construções em tempo de compilação de qualquer tipo, forma ou forma que não produzam exatamente o mesmo resultado toda vez que são avaliadas (hipoteticamente). Portanto, pode ser um bug do compilador, pode ser um caso "mal formado, sem necessidade de diagnóstico" ou pode ser algo que o padrão perdeu. No entanto, vai contra o "espírito do padrão". Sinto muito. Também gostaria de contar contadores de tempo de compilação.
bolov 5/02
@HolyBlackCat Devo confessar que estou achando muito difícil entender esse código. Parece que isso poderia evitar a necessidade de passar explicitamente um número monotonicamente crescente como parâmetro para a 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/566849
Fabio A.
@FabioA. Eu também não entendo completamente essa resposta. Desde que fiz essa pergunta, percebi que não quero mais tocar nos contadores constexpr.
HolyBlackCat
Embora esse seja um experimento divertido e pouco pensado, alguém que realmente usou esse código teria que esperar que ele não funcionasse em versões futuras do C ++, certo? Nesse sentido, o resultado se define como um bug.
Aziuth

Respostas:

5

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.

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}

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 mesmo slot<N> .

Quando slot<N>não tem valor associado, o valor associado slot<Y>é recuperado, Ysendo o índice mais alto menor do que Naquele que slot<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 quando slot<N>não tinha valor associado. Por sua vez, isso significa que todos os slots são efetivamente associados ao valor base 0.

A solução é transformar o código acima neste:

template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}

Observe que slot<N>()foi modificado em slot<N-1>(). Faz sentido: se eu quiser associar um valor slot<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 associado slot<N>deve ser um mais o valor associado slot<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:

  • O g ++ possui uma peculiaridade / bug / relaxamento que cancela o bug da minha solução e, eventualmente, faz com que o código funcione.
  • As versões clang ++> 7.0.0 são mais rígidas e não gostam do bug no código original.
  • As versões clang ++ <= 7.0.0 possuem um bug que faz com que a solução corrigida não funcione.

Resumindo, o código a seguir funciona em todas as versões do g ++ e clang ++.

#if !defined(__clang_major__) || __clang_major__ > 7
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N-1>())+1>::value) {
    return R;
}
#else
template <int N>
constexpr int next(int R = writer<N, reader(0, slot<N>())+1>::value) {
    return R;
}
#endif

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 de deduce the return type of function "counter(slot<N>)"causa it has not been defined. Eu acredito que isso é um bug , que pode ser contornado com SFINAE no resultado direto de counter(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, #ifdefpoderia vir ao resgate.

A prova está em godbolt , screnshotted abaixo.

insira a descrição da imagem aqui

Fabio A.
fonte
2
Eu acho que essa resposta meio que fecha o tópico, mas eu ainda gostaria de saber se estou certa em minha análise, portanto, esperarei antes de aceitar minha própria resposta como correta, esperando que outra pessoa passe e me dê uma pista melhor. ou uma confirmação. :)
Fabio A.