Declarations in C++
Back to Basics: Declarations in C++ - Ben Saks - CppCon 2022
Source: Back to Basics: Declarations in C++ - Ben Saks - CppCon 2022
Entities and Properties
Entity
A computer program is essentially:
- Entities
- Actions involving those entities
Entities in C++:
- function
- namespace
- object
- template
- type
- value
Properties
Properties of declared name:
| Property | Object | Function | Label |
|---|---|---|---|
| Scope | yes | yes | yes |
| Type | yes | yes | no |
| Storage duration | yes | no | no |
| Linkage | yes | yes | no |
Declarations and Definitions
Declaration tells name and type/signature (some properties). It may or may not allocate storage/provide a body.
- Definition is a declaration that actually creates the entity, that is
- object $\rightarrow$ allocate storage
- function $\rightarrow$ provide body
- types (struct) $\rightarrow$ provide type definition
- In C++, an object declaration (outside a class) is also a definition unless it contains extern specifier and no initializer:
1
2
3
int i; // definition
extern int j; // non-defining declaration
extern int k = 42; // definition
Declaration
Every object and function declaration has two main parts:
- declaration specifiers
- declarators (including declarator-id or name)
For example: static unsigned long int *x[N]
static unsigned long intare declaration specifiers*x[N]is declaratorxis declarator-id
Each declaration specifier is either type or non-type specifier.
- Type specifier: modify other type specifiers
- A sequence of keywords such as
int,unsigned,long, ordouble - an idenfier or qualified name that names a type, such as
std::string - a template specilization, such as
vector<long double>
- A sequence of keywords such as
- Non-type specifier: apply directly to the declarator-id
- a storage class specifier:
extern,static,thread_local - a function specifier:
inline,virtual - other specifier:
friend,typedef
- a storage class specifier:
A declarator is a declarator-id, possibly surrounded by operators:
| Precedence | Operator | Meaning |
|---|---|---|
| Highest | ( ) | grouping |
| [ ] ( ) | array function | |
| Lowest | * & && | pointer (lvalue) reference rvalue reference |
We read from the variable name outwards, following the precedence of operators. Examples:
int *x[N]:[]has higher precedence than*, so we readx[N]first.- This means
xis an array ofNelements. - Each element has type
int *. - So
xis an array ofNpointers toint. - Another way to see it is:
int (*(x[N])), soxis an array ofNelements, each element is a pointer toint.
int (*x)[N]- The parentheses force
*xto bind first. - This means
xis a pointer. - Then
[N]tells us it points to an array ofNelements. - So
xis a pointer to an array ofNints. - Another way to see it is:
int ((*x)[N]), so*xis an array ofNelements, each element is anint, thusxis a pointer to an array ofNints.
- The parentheses force
int *f()()has higher precedence than*, so we readf()first.- This means
fis a function. - Its return type is
int *. - So
fis a function returning a pointer toint. - Another way to see it is:
int (*(f())), sof()is a function returning a pointer toint, thusfis a function returning a pointer toint.
int (*f)()- The parentheses force
*fto bind first. - This means
fis a pointer. - Then
()tells us it points to a function. - That function returns
int. - Another way to see it is:
int ((*f)()), so*fis a function returningint, thusfis a pointer to a function returningint. Sofis a pointer to a function returningint.
- The parentheses force
Order of declaration specifiers doesn’t matter.
1
2
3
const unsigned long cul; // const unsigned long
long unsigned const cul; // same thing
unsigned const long cul; // same
const keyword
const is a type specifier, like long and unsigned, it modifies other type specifiers.
Example: const int *v[N], then const modifies int, thus
vis “array of N pointers to constint”,- not “const array of N pointers to
int”.
constandvolatileare only symbols that can appear either as declaration specifiers or in declarators.
*const turns the pointer into a const pointer, it is effectively a single operator with the same precedence as *.
Trick: Read from right to left
1
2
3
widget *const cpw // const pointer to `widget`
widget *const *pcpw // pointer to const pointer to `widget`
widget **const cpw // const pointer to (non-const) pointer to `widget`
How to declare like what you intend:
- Write the declaration without
const. - Then, place
constto the immediate right of type specifier or operator that you want it to modify.
- Example: “array of N const pointer to volatile uint32_t”
- Start by writing without
constandvolatile: “array of Nconstpointer tovolatileuint32_t”:uint_32_t *x[N] - Then add
constto the right of * andvolatileto the right of uint_32:uint_32_t volatile *const x[N]
- Start by writing without
Declarator Initializer
1
2
3
int n = 42; // "equal" initializer
int n (42); // "parenthesized" initializer
int n {42}; // "braced" initializer
constexpr keyword
constexpr is declaration specifier, it isn’t a type specifier as it modifies declarator-id not other type specifier
1
2
char constexpr | * p // constexpr pointer to char
char | *const p // const pointer to char
Both are the same but have different initialization requirements.
The initializer must be a const expression in
constexpr.
Using typename with Dependent Names
Template parameter lists use keyword typename to declare template type parameters.
You could use
classinstead oftypenamehere (but ony in template parameter lists).
1
2
template <typename T, typename P>
class widget;
Another use of typename
1
2
3
4
template <typename T>
T foo(T x) {
~~~
}
Compiler can’t generate code for an instantiation as it doesn’t know what is T yet.
Two-Phase Translation
On first reading, compiler can’t detect all possible errors, it tries to do as much checking as it can to report errors as early as possible.
- The 1st phase: compiler parses the template declaration. This happens just once for each template.
- The 2nd phase: compiler instantiates the template for a particular combination of template arguments the first time that combo is needed.
Member type
Consider this function:
1
2
3
4
5
template <typename T>
T::size_type munge(T const &a) {
T::size_type *i(T::npos);
~~~
}
This template works only for a type T that has size_type and npos as members.
Compiler only knows that T represents a type, but it doesn’t know that:
T::size_typeis supposed to be a type, orT::nposis supposed to be a constant. It can’t know until it knows the argument substituted forTin a given instantialization.
Suppose:
T::size_typeis a type, andT::nposis a type
Then it becomes a function declaration, declaring i as a function:
- with an unamed parameter of type
T::npos, - returning a “pointer to
T::size_type”.
Suppose:
T::size_typeis a type, andT::nposis a constant, object, or function (anything but a type)
Then it becomes an object declaration, declaring i as an object:
- of type “pointer to
T::size_type” - initialized with the value
T::npos.
Suppose:
T::size_typeis not a type, andT::nposis not a type
Then it becomes a multiply expression, with LHS is T::size_type, RHS is i(T::npos) might be:
- a function call, or
- a function-like cast.
Dependent vs Non-dependent name
- A name appearing in a template whose meaning depends on one or more template parameters is a dependent name.
- In the
mungetemplate:T::size_typeandT::nposare dependent names.- They depends on template type parameter
T.
- A dependent name may have different meaning of each instantiation of the template.
- In the
- Name that are not dependent are non-dependent names
- A non-dependent name has the same meaning in every instantiation of the template.
Compiler needs to know whether a dependent name such as T::size_type is indeed a type, or something else. If it’s not a type (or a template name), the compiler doesn’t care what it is.
Types — and only types — distinguish declarations from expressions.
A dependent name is assumed not a type unless the name is qualified by the keyword
typename.
The definition for the munge function template should look like:
1
2
3
4
5
template <typename T> // (1)
typename T::size_type munge(T const& a) { // (2)
typename T::size_type* i(T::npos); // (3)
~~~
}
- On (1),
typenametells the compiler thatTis a type. - On (2) and (3), the
typenameintypename T::size_typedoesn’t modifyT; it modifiessize_type.
You can’t use
classinstead oftypenamein this way.
Rvalue References vs. Forwarding References
Rvalue References
When && appears in a declarator, it usually declares an rvalue reference, as in:
1
void doIt(string &&arg); // for rvalues
An rvalue reference must bind to an rvalue.
1
2
3
4
5
string s1 = "Hello";
string s2 = "Goodbye";
doIt(s1); // Error: s1 is an lvalue
doIt(s1 + s2); // OK: s1 + s2 is an rvalue
Forwarding References
However, sometimes && in a declarator means forwarding reference rather than rvalue reference. For example:
1
2
template <typename T>
void dispatch(T &&arg); // a forwarding reference
Unlike an rvalue reference, a forwarding reference can bind to either an lvalue or an rvalue.
We can use forwarding references to write forwarding functions - that is, functions that pass (forward) their arguments to another function unmodified.
A forwarding reference “remembers” whether it’s bound to an lvalue or to an rvalue.
- We can use
std::forwardto pass that knowledge on to the forwarded-to function. - The forwarded-to function can use that knowledge to optimize, such as by using move semantics for rvalues.
How to determine Rvalue or Forwarding References
arg is a forwarding reference iff:
- arg has no cv-qualifiers
- A forwarding reference may not be declared
constorvolatile. - This is because
conston a reference type is pointless, because you cannot modify the reference itself (const (T&)is const reference toT, not reference to a const object, so it is equivalent toT&). - In this declaration,
arg2is an rvalue reference:
1
2
template <typename T>
void func2(T const &&arg2); // an rvalue reference (to const T)
- arg is in a “deduction context”
Here, arg is in a deduction context because the type T may be deduced from a template argument:
1
2
3
4
5
template <typename T>
void dispatch(T &&arg); // a forwarding reference
dispatch(3); // calls dispatch<int>
dispatch(3.5); // calls dispatch<double>
Not every part of a template is a deduction context, as this example shows:
1
2
3
4
5
template <typename T>
void dispatch(T &&arg) {
T &&temp = f(arg);
~~~
}
argis a forwarding reference, buttempis an rvalue reference - it’s not declared in a deduction context.- The type argument for
Twas determined whendispatchwas called. - Nothing inside the function can change it.
- The type argument for
Similarly, this x is an rvalue reference, not a forwarding reference:
1
2
3
4
5
6
template <typename T>
class C {
public:
void mf(T &&x);
~~~
};
- The type argument for
Tis set when theCobject is created:C<int> c; - Because
cisC<int>,c.mfexpects anint &&as its argument.
A declaration that uses the keyword
autois also a deduction context, thus it is forwarding reference:
1
2
auto &&r1 = 3; // r1 is int &&
auto &&r2 = 3.5; // r2 is double &&