How C++ Parses Contextual Keywords final and override
In C++11, the committee introduced final and
override to improve object-oriented design and catch subtle
inheritance bugs, but adding them as reserved keywords would have broken
millions of lines of legacy code where these terms were already used as
identifiers. To solve this, the language defines them as "identifiers
with special meaning"—commonly called contextual keywords. This article
explains how the C++ translation process distinguishes these identifiers
from ordinary names, how lexical analysis interacts with syntactic
parsing, and how the grammar resolves ambiguity without treating these
terms as globally reserved words.
The Backward Compatibility Dilemma
Before C++11, words like final and override
were valid names for variables, member functions, macros, or types. If
the standard had promoted them to full keywords (like
virtual, class, or const), legal
pre-C++11 code such as the following would immediately fail to
compile:
int override = 10;
void final();To prevent breaking existing codebases, the C++ standard designated
final and override as identifiers that only
act as keywords within strictly defined grammatical contexts. Everywhere
else, they behave as standard user-defined identifiers.
Lexing vs. Parsing: Keeping Identifiers General
In a standard C++ compilation pipeline, the lexical analyzer (lexer) reads source characters and groups them into tokens.
For ordinary reserved keywords, the lexer recognizes them directly:
- The text
"virtual"produces the tokenTOKEN_VIRTUAL. - The text
"const"produces the tokenTOKEN_CONST.
For contextual keywords, the lexer does not generate distinct keyword
tokens. Instead, when encountering "override" or
"final", it yields a general TOKEN_IDENTIFIER
whose string value is recorded as "override" or
"final". Because the lexer lacks knowledge of grammar
hierarchy, it remains agnostic about the semantic intent of the
word.
Grammar-Driven Resolution in the Parser
The syntactic analyzer (parser) is responsible for interpreting the meaning of these identifiers. It does this by checking the token stream against the C++ grammar rules defined in the ISO standard.
The grammar defines specific slots where these identifiers take on special semantics:
Virtual Specifiers (
virt-specifier): Occurs at the end of a member function declaration, following the parameter list, cv-qualifiers, and exception specifications:virt-specifier: override finalClass Virtual Specifiers (
class-virt-specifier): Occurs immediately after the class name (and optional attributes) in a class-head:class-virt-specifier: final
When the parser encounters a TOKEN_IDENTIFIER at one of
these exact grammatical positions, it inspects the identifier’s string
value. If the string is "override" or "final",
the parser applies the appropriate semantic constraint:
override: Ensures that the member function is virtual and overrides a virtual function from a base class.finalon a function: Prevents further overriding in derived classes.finalon a class: Prevents the class from being used as a base class.
If the identifier appears in any other grammatical position—such as a type specifier, variable name, or function name—the parser treats it like any other user-defined identifier.
Disambiguating Edge Cases
Because parsing is contextual, valid C++ code can combine both usages within the same scope without collision:
struct Base {
virtual void foo() {}
};
struct override : Base { // 'override' is a class name here
int final = 5; // 'final' is a member variable here
// The first 'override' is the return type (if a type named 'override' exists),
// and the trailing 'override' is the contextual keyword.
void foo() override {
int override = 20; // Local variable
}
};
struct Derived final : override { // 'final' is a contextual specifier; 'override' is the base class
void foo() final override {} // Both used as virt-specifiers
};When parsing void foo() override, the parser reaches the
end of the declarator where a virt-specifier-seq is
grammatically expected. The token matches the text
"override", so it consumes it as an instruction to verify
the base class virtual table. When parsing
int override = 20;, the parser expects a declarator-id
following the type int, meaning the token is consumed as an
ordinary variable name.
By deferring resolution to the syntactic parsing stage rather than the lexical analysis stage, C++ compilers maintain complete backward compatibility while extending the language's type-checking capabilities.