Este post fala sobre um método que conta o número de membros de uma classe:
struct UniversalType { template<typename T> operator T(); };
template<typename T, typename... A0>
consteval auto MemberCounter(auto ...c0)
{
if constexpr (requires { T{ {A0{}}..., {UniversalType{}}, c0... }; } == false
&& requires { T{ {A0{}}..., c0..., UniversalType{} }; } == false )
{
return sizeof...(A0) + sizeof...(c0);
}
else if constexpr (requires { T{ {A0{}}..., {UniversalType{}}, c0... }; })
{
return MemberCounter<T,A0...,UniversalType>(c0...);
}
else
{
return MemberCounter<T,A0...>(c0...,UniversalType{});
}
}
using TestType = struct { int x[3]; float y; char z; };
static_assert (MemberCounter<TestType>() == 3);
int main() {}
Em particular, as duas requires
cláusulas a seguir me intrigam um pouco, pois misturam chaves simples e duplas:
requires { T{ {A0{}}..., {UniversalType{}}, c0... }; }
requires { T{ {A0{}}..., c0..., UniversalType{} }; }
Pergunta: que tipo de itens eles permitem combinar exatamente?
A dica está em
TestType
, que foi deliberadamente projetado para demonstrar o problema que o aninhamento{}
resolve.A premissa de contar variáveis de membro é verificar o número máximo de argumentos que o tipo aceitará na inicialização agregada. Isso pode ser feito sem saber o tipo dos membros por meio de
UniversalType
, que pode fingir ser qualquer tipo. Por exemploque nos diz que
X
tem duas variáveis de membro.Há um problema com isso: a inicialização agregada inicializa subobjetos recursivamente. Então, por exemplo
Para remediar isso, um aninhado extra
{}
é introduzido. Cada{UniversalType{}}
or{A0{}}
pode inicializar apenas um único subobjeto, mesmo que seja um agregado, resolvendo o problema.Embora pareça que
{UniversalType{}}
pode inicializar qualquer tipo, há uma exceção: um agregado vazio. Então precisamos testar para ambos{UniversalType{}}
eUniversalType{}
.Dito isto, há vários casos extremos que estão errados com essa implementação.
Para
A
,A{UniversalType{}}
falha porque o segundo membrob
não pode ser inicializado por padrão, então o contador retorna0
. Isso pode ser corrigido contando para baixo em vez de para cima.Para
C
, o problema é um pouco mais fundamental. A implementação não tem como lembrar onde os agregados vazios estão. Como{A0{}}
precedec0
, nenhum agregado pode seguir um agregado vazio.