Skip to content
C++
Language
Intermediate

Elaborated Type Specifiers

A class-key or enum keyword prefix that disambiguates a type name from other names in scope, enables forward declarations, and resolves lookup in complex contexts.

Elaborated Type Specifiersince C++98

An elaborated type specifier is a type name prefixed by class, struct, union, or enum that unambiguously refers to a type even when a non-type name of the same identifier is in scope, and that can introduce a forward declaration of a class type as a side effect.

Overview

Elaborated type specifiers exist to solve two distinct but related problems. The first is disambiguation: C++ allows a non-type name (a variable, function, or enumerator) to shadow a type name in the same scope. The second is forward declaration: you often need to name a type before its full definition is available, particularly when building mutually recursive data structures or breaking circular header dependencies.

The syntax takes one of these forms:

  • class identifier / struct identifier / union identifier β€” refers to or introduces a class type
  • enum identifier β€” refers to a previously declared enumeration (cannot introduce a new one)

These prefixes are collectively called class-keys for class types, and enum for enumeration types. In every context where a simple name would be ambiguous or insufficient, the elaborated form is unambiguous.

Syntax

cpp
// Basic elaborated forms
struct Point { int x, y; };
class Engine { /* ... */ };
union Variant { int i; float f; };
enum Color { Red, Green, Blue };

void f() {
    int Point = 42;           // shadows the struct name

    // Point p;               // error: Point is the int variable here
    struct Point p = {1, 2};  // OK: elaborated specifier bypasses the variable
    (void)p;
}

The class and struct keywords are interchangeable in elaborated type specifiers β€” they both refer to any non-union class type regardless of how it was originally declared:

cpp
struct Foo {};
class Foo bar;   // OK: 'class' and 'struct' are interchangeable here
struct Foo baz;  // also OK

union must be used for unions, and enum must be used for enumerations. Mismatching the keyword is ill-formed:

cpp
enum class Status { OK, Fail };  // scoped enum (C++11)

enum Status s = Status::OK;       // OK
// enum class Status t = Status::OK; // error: 'enum class' is not a valid elaborated specifier

Forward Declarations

The most common use of elaborated type specifiers is to forward-declare a class type. A declaration that consists solely of an elaborated type specifier for a class introduces the name into the nearest enclosing namespace or block scope:

cpp
// Forward declarations in a header β€” no #include needed
struct Node;       // declares Node at namespace scope
class Allocator;   // declares Allocator at namespace scope

struct Node {
    int value;
    Node* next;      // OK: pointer to incomplete type
    struct Data* metadata; // OK: also forward-declares 'Data' at global scope
};

This form is the standard technique for breaking circular dependencies between headers. As long as you only need a pointer or reference to the type β€” not its size or members β€” an elaborated forward declaration suffices.

Note that enum types cannot be forward-declared with an elaborated type specifier alone. Since C++11, opaque enum declarations (enum E : int;) serve that purpose and produce a complete type immediately.

Name Disambiguation

When a non-type name shadows a class name, an elaborated type specifier instructs the compiler to skip non-type names during lookup:

cpp
class Buffer {
public:
    class Config {};
private:
    int Config;  // data member shadows the nested class name
};

void setup(Buffer& b) {
    // Buffer::Config cfg;         // error: finds the int member
    class Buffer::Config cfg;      // OK: non-type member is ignored
    (void)cfg;
}

This also applies at function scope, where local variables can shadow type names from outer scopes:

cpp
struct Connection {};

void connect() {
    int Connection = -1;           // shadows the struct

    // Connection c;               // error
    struct Connection c;           // OK: resolves to the struct
    (void)c;
}

Qualified elaborated type specifiers β€” those using :: β€” follow qualified name lookup rules, but still suppress non-type names at each lookup step.

Elaborated Specifiers in Templates

Inside a template, elaborated type specifiers interact with the injected class name and with dependent name rules:

cpp
template <typename T>
struct List {
    struct List* head;   // OK: injected-class-name 'List' is found first

    struct Node* node;   // OK: forward-declares 'Node' at the enclosing namespace scope
                         // (not inside the template instantiation)
};

List<int>::Node* p = nullptr;  // 'Node' is at global/namespace scope, not List<int>::Node

A subtle restriction: a template type parameter cannot appear in an elaborated type specifier. The canonical friend declaration form for a type parameter is the unelaborated friend T; (C++11), not friend class T;:

cpp
template <typename T>
class Secure {
    // friend class T;  // error: type parameter in elaborated-type-specifier
    friend T;           // OK (C++11): grants T friendship directly
};

Examples

Resolving a name conflict from a legacy C struct typedef:

cpp
// C-originated header (cannot modify)
typedef struct tagRect { int l, t, r, b; } RECT;
int Rect = 0; // also in scope from some macro-heavy header

void process() {
    struct tagRect bounds = {0, 0, 100, 200}; // bypasses the int 'Rect'
    // ...
}

Mutually recursive data structures across headers:

cpp
// graph_node.h
struct Edge;  // forward declaration β€” no edge.h include needed

struct GraphNode {
    int id;
    Edge* edges;
    std::size_t edge_count;
};

// edge.h
#include "graph_node.h"

struct Edge {
    GraphNode* from;
    GraphNode* to;
    double weight;
};

Using elaborated specifiers in function signatures to avoid headers:

cpp
// In a .cpp file β€” avoids polluting the header with an #include
void enqueue(struct Task* t);
void enqueue(struct Task* t) {
    // full definition of Task available here via its own header
}

Best Practices

Keep elaborated type specifiers in forward-declaration contexts (struct Foo; at namespace scope) as the primary tool for reducing header coupling. Avoid relying on them for disambiguation β€” if a non-type name is shadowing a type name in your own code, renaming one of them is almost always the better fix.

Do not use struct/class prefixes on every type reference as a C habit. Modern C++ typedef-free struct definitions do not require it, and sprinkling elaborated prefixes throughout call sites adds noise without benefit. Reserve them for the situations described above.

When writing template code, prefer friend T; over friend class T; for type-parameter friends. The elaborated form is ill-formed for template parameters and the compiler error can be confusing without knowing this rule.

Common Pitfalls

Using enum class as an elaborated specifier. The token sequence enum class is only valid in an enum declaration, never in a reference to an existing scoped enum:

cpp
enum class Dir { N, S, E, W };  // C++11

// enum class Dir d = Dir::N;   // error: 'enum class' is not an elaborated-type-specifier
enum Dir d = Dir::N;             // OK: just 'enum' is the correct prefix

Assuming the forward-declared name enters the template's scope. As shown above, struct Foo* inside a class template injects Foo at the enclosing namespace, not as a nested type. Code that later tries Template<T>::Foo will fail.

Introducing a name at the wrong scope. If you write struct Foo* p; inside a function body where Foo has not yet been declared anywhere, Foo is introduced at the nearest enclosing namespace scope β€” not at function scope. This is a well-defined but non-obvious rule that can surprise readers:

cpp
void f() {
    struct Hidden* p = nullptr;  // 'Hidden' is declared at namespace scope, not inside f()
}

Hidden* q = nullptr;  // compiles β€” Hidden is globally visible after f's declaration

See Also

  • reference/language/adl β€” how unqualified name lookup interacts with elaborated specifiers
  • reference/language/dependent-names β€” why dependent type names require typename, not elaborated specifiers
  • reference/language/class-template β€” injected class names and their interaction with elaborated forms
  • reference/language/conflicting-declarations β€” broader rules governing name shadowing and redeclaration