Fundo
Os tipos C++ incorporados não têm tamanhos definidos (apenas um tamanho mínimo), então exigimos que todos os tipos inteiros usem typedefs explicitamente definidos para que o tamanho seja o mesmo em todas as plataformas, compiladores e arquiteturas de CPU. A única exceção a isso é size_t
, considerando que são os tipos de retorno de operações comuns como std::vector::size()
.
Então, estamos procurando maneiras de detectar via clang-tidy quando o uso de size_t
causaria um aviso de "conversão estreita" no MSVC em arquiteturas x86.
O Problema
Dada uma função como esta:
template <typename T>
void foo()
{
int64_t i{0};
T v = i;
}
Estou tentando diferenciar quando o varDecl
for v
é criado por uma chamada para
foo<size_t>()
em oposição ao não problemático foo<uint64_t>()
.
O dump AST para o acima no Compiler Explorer :
|-FunctionTemplateDecl <line:4:1, line:9:1> line:5:6 foo
| |-TemplateTypeParmDecl <line:4:11, col:20> col:20 referenced typename depth 0 index 0 T
| |-FunctionDecl <line:5:1, line:9:1> line:5:6 foo 'void ()'
| | `-CompoundStmt <line:6:1, line:9:1>
| | |-DeclStmt <line:7:3, col:16>
| | | `-VarDecl <col:3, col:15> col:12 referenced v 'uint64_t':'unsigned long' listinit
| | | `-InitListExpr <col:13, col:15> 'uint64_t':'unsigned long'
| | | `-ImplicitCastExpr <col:14> 'uint64_t':'unsigned long' <IntegralCast>
| | | `-IntegerLiteral <col:14> 'int' 3
| | `-DeclStmt <line:8:3, col:12>
| | `-VarDecl <col:3, col:11> col:5 var 'T' cinit
| | `-DeclRefExpr <col:11> 'uint64_t':'unsigned long' lvalue Var 0xcf7fc88 'v' 'uint64_t':'unsigned long'
| `-FunctionDecl <line:5:1, line:9:1> line:5:6 used foo 'void ()' implicit_instantiation
| |-TemplateArgument type 'unsigned long'
| | `-BuiltinType 'unsigned long'
| `-CompoundStmt <line:6:1, line:9:1>
| |-DeclStmt <line:7:3, col:16>
| | `-VarDecl <col:3, col:15> col:12 used v 'uint64_t':'unsigned long' listinit
| | `-InitListExpr <col:13, col:15> 'uint64_t':'unsigned long'
| | `-ImplicitCastExpr <col:14> 'uint64_t':'unsigned long' <IntegralCast>
| | `-IntegerLiteral <col:14> 'int' 3
| `-DeclStmt <line:8:3, col:12>
| `-VarDecl <col:3, col:11> col:5 var 'unsigned long' cinit
| `-ImplicitCastExpr <col:11> 'uint64_t':'unsigned long' <LValueToRValue>
| `-DeclRefExpr <col:11> 'uint64_t':'unsigned long' lvalue Var 0xcf8a3a8 'v' 'uint64_t':'unsigned long'
O que eu tentei
Posso ver que há uma instanciação do modelo de função sob o
FunctionTemplateDecl
nó, e parecia que substTemplateTypeParmType()
e hasReplacementType()
eram uma combinação promissora para obter o tipo de
varDecl
, assim:
m varDecl(
hasType(substTemplateTypeParmType(
hasReplacementType(qualType())
))
)
...entretanto, o tipo de substituição já foi "desaçucarado" naquele ponto, e perde o size_t
nome da declaração. Isso apesar do fato de a documentação dos SubstTemplateTypeParmType
estados:
Eles são usados somente para registrar que um tipo foi originalmente escrito como um parâmetro de tipo de modelo; portanto, eles nunca são canônicos.
...e a classe especificamente tem métodos isSugared()
e desugar()
, o que me leva a crer que o tipo deve ser adoçado ainda com o
size_t
typedef.
(Talvez eu esteja errado ao supor que o tipo totalmente desaçucarado é o tipo "canônico"?)
O que estou perguntando
Minhas suposições de que SubstTemplateTypeParmType
o tipo açucarado ainda deve retornar estão incorretas?
Existe uma maneira de percorrer de varDecl
, voltar para
FunctionTemplateDecl
, obter a especialização que foi usada e obter o tipo com açúcar de lá?
As especializações de modelo usam tipos canônicos
Seu objetivo, pelo que entendi, é distinguir a especialização
foo<uint64_t>
da especializaçãofoo<size_t>
, quando a plataforma de destino usaunsigned long
para ambosuint64_t
esize_t
.Isso não é possível de forma direta porque ambos são nomes para a mesma entidade; por exemplo, eles teriam o mesmo endereço na memória. Como a maioria (todos?) dos compiladores C++, a representação de uma especialização de template do Clang usa argumentos de template que se referem a tipos canônicos, onde um tipo canônico é o representante escolhido de sua classe de equivalência semântica (aqui, o tipo embutido
unsigned long
).O dump AST na questão descreve isso com precisão:
Dentro do corpo de instanciação, o tipo de
var
é apenas (aSubstTemplateTypeParmType
que se refere a)unsigned long
. O fato de que o argumento de especialização foi escrito originalmentesize_t
desaparece quando a maquinaria de instanciação começa a funcionar. Consequentemente, um AST matcher não consegue detectar essa informação faltante.(Observação: o código na pergunta é um pouco diferente do código que foi usado para criar o dump AST. No código da pergunta, essa variável é chamada
v
, enquanto ela foi chamadavar
quando o dump foi criado.)Mas
SubstTemplateTypeParmType
não é canônico?Isso mesmo, mas significa apenas que ele próprio não é canônico; o tipo ao qual ele se refere geralmente é canônico. (Acho que as únicas exceções são quando o tipo subjacente também é dependente, o que surge quando, por exemplo, um modelo de classe que contém um modelo de função membro é instanciado).
SubstTemplateTypeParmType
Elaborando um pouco,
SubstTemplateTypeParmType
não é canônico porque é simplesmente um wrapper para, e semanticamente equivalente a, algum outro tipo. O tipo dentro é normalmente canônico. De certa forma,SubstTemplateTypeParmType
é comoTypedefType
, que também é um wrapper não canônico para outro tipo (que pode ou não ser canônico).Por exemplo, se definirmos um modelo:
então implicitamente instancie-o dizendo
foo<unsigned long>
, então dentro da instanciação resultante, a declaração deTptr
será umTypedefDecl
cujogetTypeForDecl()
é aTypedefType
apontando para aPointerType
apontando para aSubstTemplateTypeParmType
apontando para aBuiltinType
. Somente o finalBuiltinType
será canônico nesse caso.Existe outra maneira de fazer isso?
Sim, mas provavelmente não com comparadores AST.
O template-id (nome da especialização)
foo<size_t>
dentro da expressãofoo<size_t>()
é umDeclRefExpr
whosetemplate_arguments()
refer tosize_t
(que não é evidente no dump da árvore AST; usei minha própria ferramenta que imprime mais detalhes para investigar isso) e seusgetDecl()
pontos na instanciação (o dump da árvore mostra isso).Então, dada uma instanciação que usa a
SubstTemplateTypeParmType
de uma forma potencialmente problemática:Pesquise tudo
DeclRefExpr
na unidade de tradução cujosgetDecl()
pontos estejam nessa instanciação.Para cada um, examine os argumentos do modelo para obter aquele com o índice adequado.
Verifique se esse argumento é
size_t
, e informe se for.Este procedimento seria razoavelmente simples de fazer usando a API Clang C++. Suspeito que não pode ser feito usando apenas AST matchers, mas acho que isso também depende exatamente de como você está identificando usos de tipos potencialmente problemáticos.