A declaration introduces a name and says what it means. Most are also definitions, which create the thing named [basic.def]. A translation unit is a series of declarations.
Specifiers
simple-declaration:
decl-specifier-seq [init-declarator-list] ;
init-declarator:
declarator [initializer]
A declaration has a sequence of specifiers, then a list of declarators, each of
which names one entity [dcl.spec]. int* a, b; declares a pointer
a and an int b, because the *
belongs to the declarator.
| Group | Specifiers | Section |
|---|---|---|
| Storage class | static thread_local extern mutable | [dcl.stc] |
| Function | virtual explicit | [dcl.fct.spec] |
| Type alias | typedef | [dcl.typedef] |
| Constant evaluation | constexpr consteval constinit | below |
| Other | friend inline | [dcl.friend], [dcl.inline] |
| Type | The type names, const, volatile, auto, decltype(...) | [dcl.type] |
Storage duration
An object’s storage duration decides how long its storage lasts [basic.stc].
| Duration | Lasts | Applies to |
|---|---|---|
| Static | The whole program | Namespace-scope variables, and those declared static or extern |
| Thread (C++11) | The lifetime of a thread; each thread has its own | Variables declared thread_local |
| Automatic | The enclosing block | Local variables without static, extern or thread_local |
| Dynamic | From allocation until release | Objects from new and similar allocation |
A local static variable is initialised the first time control passes
through its declaration. Since C++11 this is thread-safe [stmt.dcl].
Type aliases
typedef unsigned long ulong_t; // C-style alias
using ulong2 = unsigned long; // alias-declaration (C++11)
template<class T>
using Ptr = T*; // alias template: typedef cannot do this
Declarators
A declarator gives a name and modifies the type from the specifiers [dcl.decl], [dcl.meaning].
| Form | Meaning | Section |
|---|---|---|
*d | Pointer | [dcl.ptr] |
&d, &&d | Lvalue or rvalue reference | [dcl.ref] |
C::*d | Pointer to member of C | [dcl.mptr] |
d[n], d[] | Array | [dcl.array] |
d(params) | Function | [dcl.fct] |
(d) | Grouping | [dcl.meaning] |
A declarator is read from the name outwards, with suffixes binding tighter than prefixes:
int x; // int
int* p; // pointer to int
int a[3]; // array of 3 int
int* b[3]; // array of 3 pointers to int
int (*c)[3]; // pointer to array of 3 int
int f(int); // function taking int, returning int
int (*g)(int); // pointer to function taking int, returning int
int (&h)(int) = f; // reference to function
int* (*k[2])(int); // array of 2 pointers to function returning int*
int C::*m; // pointer to int member of C
int (C::*n)(int); // pointer to member function of C
Ambiguity: the vexing parse
Anything that can be parsed as a declaration is one [dcl.ambig.res]. This catches code that was meant as an object definition. Brace initialisation avoids it.
Widget a(); // declares a function a, not an object
Widget b{}; // an object, value-initialised
Widget c(Widget()); // declares a function c taking a function pointer
Widget d{Widget()}; // an object
Initialisation
C++ has several forms of initialisation, chosen by syntax [dcl.init].
| Form | Syntax | Effect |
|---|---|---|
| Default | T x; | Calls the default constructor of a class. Leaves other scalar objects with an indeterminate value, unless they have static or thread storage duration |
| Value | T x{}; or T() | Calls the default constructor if there is a user-provided one. Otherwise zero-initialises first |
| Zero | Implicit, for static and thread storage | Scalars are set to the zero value of their type |
| Direct | T x(a); | Calls a constructor, including an explicit one |
| Copy | T x = a; | Uses only non-explicit conversions |
| Direct-list | T x{a}; | List initialisation: no narrowing |
| Copy-list | T x = {a}; | List initialisation, ill-formed if the chosen constructor is explicit |
| Aggregate | T x{a, b}; T x{.m = a}; | See below |
| Reference | T& r = x; | Binds the reference; see [dcl.init.ref] |
Reading an object with an indeterminate value is undefined behaviour, with narrow
exceptions for unsigned char and std::byte.
List initialisation
List initialisation (C++11) is brace initialisation [dcl.init.list]. It prevents
narrowing. If a class has an
std::initializer_list constructor, that constructor is preferred
when it is viable.
std::vector<int> a(3, 5); // three elements, each 5
std::vector<int> b{3, 5}; // two elements: 3 and 5
int n{3.14}; // error: narrowing
int m(3.14); // allowed: m is 3
Aggregates
An aggregate is an array, or a class with no user-declared or inherited constructors, no private or protected direct non-static data members, no virtual functions, and no virtual, private or protected base classes [dcl.init.aggr]. An aggregate is initialised member by member.
struct Point { int x; int y = 7; };
Point a{1, 2}; // x = 1, y = 2
Point b{1}; // x = 1, y = 7 (default member initialiser)
Point c{.x = 1, .y = 2}; // designated initialisers (C++20)
Point d(1, 2); // parenthesised aggregate initialisation (C++20)
int v[3] = {1, 2}; // v[2] is zero-initialised
int w[] = {1, 2, 3}; // the bound is deduced: int[3]
- Designated initialisers must follow declaration order, and cannot be nested or mixed with positional ones.
- Members with no initialiser are initialised from their default member initialiser, or copy-initialised from an empty list.
- Base classes of an aggregate are treated as initial elements (C++17).
Character arrays
A character array can be initialised from a string literal [dcl.init.string].
The bound is deduced to include the terminator, and char s[3] = "abc"; is
an error since it has no room for it.
Structured bindings
A structured binding (C++17) declares several names that refer to parts of one
object [dcl.struct.bind]. A hidden variable e holds the initialiser, and
each name refers to one element or member of it.
attribute-specifier-seq? decl-specifier-seq ref-qualifier? [ identifier-list ] initializer ;
initializer: = expression | { expression } | ( expression )
In this form the square brackets are literal, and ? marks the optional
parts.
| Case | Applies when | Names refer to |
|---|---|---|
| Array | The initialiser is an array | Elements of the array, and the count of names must match its size |
| Tuple-like | std::tuple_size<E> is a complete type with a member value | The results of e.get<i>() if a suitable member exists, otherwise of get<i>(e) found by argument-dependent lookup |
| Data members | Otherwise | Each non-static data member, in order. All must be accessible where the binding is written and be direct members of one class in the hierarchy. An anonymous union member is not allowed |
std::map<std::string, int> counts;
for (auto& [key, value] : counts) // references to the pair's members
value += 1;
auto [first, second] = std::pair{1, 2.0};
struct Point { int x; int y; };
const auto [x, y] = Point{1, 2}; // x and y are const
int arr[2] = {1, 2};
auto& [a, b] = arr; // a and b alias arr's elements
auto is required. The qualifiers and reference applied to it affect the hidden variable,
not the names directly. A structured binding cannot be constexpr in C++23 and,
before C++20, could not be captured by a lambda.
constexpr, consteval and constinit
| Specifier | Applies to | Requires | Since |
|---|---|---|---|
constexpr | Variable | Constant-initialised, and implicitly const | C++11 |
constexpr | Function | May be evaluated at compile time; implicitly inline | C++11 |
consteval | Function | Every call must produce a constant: an immediate function | C++20 |
constinit | Static or thread storage variable | Constant initialisation; not implicitly const | C++20 |
constexpr int square(int n) { return n * n; }
constexpr int a = square(4); // evaluated at compile time
consteval int cube(int n) { return n * n * n; }
int b = cube(3); // the call must be constant
constinit int counter = 0; // no dynamic initialisation
void bump() { ++counter; } // not const
void f() {
if consteval { // C++23
/* compile-time path */
} else {
/* run-time path */
}
}
What constexpr functions may contain has grown over time.
| Standard | Added to constexpr functions |
|---|---|
| C++11 | A function body that is essentially one return statement |
| C++14 | Local variables, loops, and mutation of local state |
| C++17 | constexpr lambdas |
| C++20 | Virtual calls, try blocks, dynamic_cast and typeid, changing the active member of a union, and transient allocation with new, provided it is freed before evaluation ends |
| C++23 | Non-literal variables, labels and goto, provided they are not evaluated during constant evaluation; static and thread_local variables that are usable in constant expressions |
A constant expression is one that the compiler can evaluate entirely during translation, without undefined behaviour [expr.const]. An expression with undefined behaviour is not a constant expression, so constant evaluation can detect it.
constinit
constinit asserts that a variable with static or thread storage duration
has constant initialisation [dcl.constinit]. This avoids the initialisation-order
problems of dynamic initialisation, described in
File structure. Unlike
constexpr, the variable may be changed later.
Attributes
An attribute has the form [[name]] or [[ns::name(args)]]
[dcl.attr]. A compiler ignores attributes it does not recognise, except that an unknown
attribute may cause a warning. [[using ns: a, b]] (C++17) shares a namespace
between several attributes.
| Attribute | Meaning | Since |
|---|---|---|
[[noreturn]] | The function does not return | C++11 |
[[carries_dependency]] | A dependency chain continues through the function | C++11 |
[[deprecated]], [[deprecated("why")]] | Use produces a warning | C++14 |
[[fallthrough]] | A switch case falls through on purpose | C++17 |
[[maybe_unused]] | Suppresses unused warnings | C++17 |
[[nodiscard]], [[nodiscard("why")]] | Warns if the result is discarded (reason since C++20) | C++17 |
[[likely]], [[unlikely]] | Optimisation hints on a path of execution | C++20 |
[[no_unique_address]] | A member need not have a distinct address | C++20 |
[[assume(expr)]] | The expression would be true here; if it is false the behaviour is undefined | C++23 |
alignas looks like an attribute but is a keyword with its own
grammar [dcl.align].
Language linkage
A linkage specification sets the language linkage of function and variable names
[dcl.link]. Implementations must support "C" and "C++", and
other strings are conditionally supported. A header usually guards this for use from both languages.
#ifdef __cplusplus
extern "C" {
#endif
int c_function(int value);
#ifdef __cplusplus
}
#endif
Elsewhere in a declaration, asm("text") is a conditionally supported
way to embed assembly language [dcl.asm].