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++98An 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 typeenum 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
// 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:
struct Foo {};
class Foo bar; // OK: 'class' and 'struct' are interchangeable here
struct Foo baz; // also OKunion must be used for unions, and enum must be used for enumerations. Mismatching the keyword is ill-formed:
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 specifierForward 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:
// 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:
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:
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:
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>::NodeA 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;:
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:
// 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:
// 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:
// 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:
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 prefixAssuming 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:
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 declarationSee Also
reference/language/adlβ how unqualified name lookup interacts with elaborated specifiersreference/language/dependent-namesβ why dependent type names requiretypename, not elaborated specifiersreference/language/class-templateβ injected class names and their interaction with elaborated formsreference/language/conflicting-declarationsβ broader rules governing name shadowing and redeclaration