Better C++ Syntax Highlighting - Part 6: Templates
Templates and concepts make up a huge part of modern C++. In this post, we’ll cover how to annotate template declarations, template parameters, and C++20 concepts.
Consider the following example:
And corresponding AST:
Template declarations
Template declarations are represented by several node types:
ClassTemplateDeclnodes for primary class templates,ClassTemplatePartialSpecializationDeclnodes for partial specializations,ClassTemplateSpecializationDeclnodes for explicit specializations, andTemplateTypeParmDeclnodes for template parameters
We don’t actually need to define new visitors for the template class declarations themselves.
Each ClassTemplateDecl, ClassTemplatePartialSpecializationDecl, and ClassTemplateSpecializationDecl node contains a nested CXXRecordDecl representing the underlying class.
This is already handled by our existing VisitCXXRecordDecl visitor.
However, we do need to annotate template parameters, which are represented by TemplateTypeParmDecl nodes.
The visitor signature looks like this:
And implementation:
Template parameters represent types and are annotated with class-name.
This works for both template functions and class declarations:
Concepts
C++20 introduced concepts for constraining template parameters.
Consider the following example:
And corresponding AST:
Concept-related declarations and expressions are represented by several node types:
ConceptDeclnodes for concept definitions,ConceptSpecializationExprnodes for concept constraints used in templates, andRequiresExprnodes for expressingrequirements in concept definitions.
We’ll need to annotate both the concept declarations and any uses of concepts as constraints within these nodes.
Concept declarations
Concept declarations are represented by ConceptDecl nodes.
We’ll annotate concept declarations and any uses of concepts as constraints using the same structure we’ve seen for other visitors:
Concept declarations are annotated with the concept tag:
Concept specializations
Constraints on concept definitions are captured by ConceptSpecializationExpr nodes.
Examples of these are the Iterable requirement on the Container concept definition and the Container specialization on the print() function.
The visitor for this node is straightforward:
The location at which to insert the annotation is retrieved via the handy getConceptNameLoc() function.
The name of the concept itself is retrieved from the declaration using getNamedConcept().
As with concept declarations, concepts in specialization expressions are annotated with the concept tag:
requires expressions
For annotating concepts in type constraint expressions, such as the std::same_as concept in the above example, we’ll need to implement a visitor for RequiresExpr nodes:
These nodes represent the body of a requires clause or expression.
This visitor is a bit more involved than others in this section.
We’ll use the getRequirements() function to iterate over each constraint in the requires clause.
For each requirement, we check whether it is an ExprRequirement, which is a constraint on an expression that may include a return type requirement.
If the expression includes a type constraint (e.g. -> std::same_as<decltype(std::end(container))>) we extract it using getReturnTypeRequirement().getTypeConstraint().
From there, we retrieve the associated concept’s name and source location as in the previous section using getNamedConcept() and getConceptNameLoc().
Concept references in type constraint expressions are also annotated as concepts:
Dependent calls and members
In templates and concepts, unresolved expressions cannot always be fully resolved during parsing due to the dependency on an unknown type T.
These expressions are represented by:
UnresolvedLookupExprnodes for ambiguous function calls, andDependentScopeDeclRefExprnodes for dependent member calls.
These often appear as child nodes of a CallExpr.
For unresolved calls, getCalleeDecl() returns nullptr, which causes issues with our existing VisitCallExpr implementation.
Good examples of this are the std::begin(...) and std::end(...) functions - the function this call resolves to differs based on the type of container that is passed in.
We can handle these nodes by extending our existing VisitCallExpr visitor to explicitly account for these types of expressions.
We’ll start off by doing some dynamic casting to determine the kind of function call being processed:
We retrieve the callee with the getCallee() function.
Instead of accessing the name of the function through its declaration, we’ll instead annotate just the token at the source location of the call.
For UnresolvedLookupExpr nodes, this is done via the getNameLoc() function.
Retrieving the name through the function declaration may not always return the same name as the function used at the call site.
The function name for a custom dereference operator, for example, returns operator* (and not the expected * of the actual call).
Member calls like T::function() appear as DependentScopeDeclRefExpr nodes.
DependentScopeDeclRefExpr represents a qualified reference to a member of a dependent type, where the type of T is unknown during parsing.
For example, in templates where T::value_type or T::function() is written, Clang cannot resolve whether this refers to a type, member, or function until the template is instantiated.
Within a CallExpr, however, if the callee is a DependentScopeDeclRefExpr, we know it’s being invoked as a function, making it safe to annotate:
We’ll use a similar approach here and annotate just the token at the call location.
DependentScopeDeclRefExpr nodes outside of function calls are purposefully left unhandled to avoid ambiguity.
The annotation for a type is different from the annotation for a member.
Direct member function calls
Function calls made directly through an object or reference, such as container.size() or container.capacity() from the example above, are represented by CXXDependentScopeMemberExpr nodes.
These nodes appear when the compiler cannot resolve the exact type of the object at parse time, but the syntax clearly indicates a member function call.
We annotate these the same way as in the earlier sections:
The source location of the call is retrieved using the getMemberLoc() function.
For all other cases, we’ll fall back to our existing approach of annotating the function call using token matching.
As before, functions are annotated with the function tag:
Dependent member access
The last node for this section is the CXXDependentScopeMemberExpr, which represents a member access where the referenced member cannot be fully resolved.
In the previous section, we annotated this case for CallExpr expressions, which represented class member function calls.
A standalone VisitCXXDependentScopeMemberExpr visitor catches dependent references to (non-static) class members:
Dependent class members are annotated with member-variable.
For the sake of this example, let’s add a requirement that our Container needs to have a public data member:
Styling
The final step is to add a definition for the concept CSS style:
The other annotations already have existing CSS style implementations.
In addition to annotating template classes, functions, and parameters, we’ve added support for annotating concept definitions, specializations, and references to functions and concepts in requires clauses.
In the <LocalLink text={“next post”} to={“Better C++ Syntax Highlighting - Part 7: Types”}>, we’ll implement adding annotations for type references in variable declarations, function parameters and return values, template arguments, and more.
Thanks for reading!