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 token TOKEN_VIRTUAL.
  • The text "const" produces the token TOKEN_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:

  1. 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
        final
  2. Class 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.
  • final on a function: Prevents further overriding in derived classes.
  • final on 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.