2.5. Dealing with Types
As our programs get more complicated, we’ll see that the types we use also get more complicated. Complications in using types arise in two different ways. Some types are hard to “spell.” That is, they have forms that are tedious and error-prone to write. Moreover, the form of a complicated type can obscure its purpose or meaning. The other source of complication is that sometimes it is hard to determine the exact type we need. Doing so can require us to look back into the context of the program.
2.5.1. Type Aliases
A type alias is a name that is a synonym for another type. Type aliases let us simplify complicated type definitions, making those types easier to use. Type aliases also let us emphasize the purpose for which a type is used.
We can define a type alias in one of two ways. Traditionally, we use a typedef:
typedef double wages; // wages is a synonym for double
typedef wages base, *p; // base is a synonym for double, p for double*
The keyword typedef
may appear as part of the base type of a declaration (§ 2.3, p. 50). Declarations that include typedef
define type aliases rather than variables. As in any other declaration, the declarators can include type modifiers that define compound types built from the base type of the definition.
The new standard introduced a second way to define a type alias, via an alias declaration:
using SI = Sales_item; // SI is a synonym for Sales_item
An alias declaration starts with the keyword using
followed by the alias name and an =
. The alias declaration defines the name on the left-hand side of the =
as an alias for the type that appears on the right-hand side.
A type alias is a type name and can appear wherever a type name can appear:
wages hourly, weekly; // same as double hourly, weekly;
SI item; // same as Sales_item item
Pointers, const
, and Type Aliases
TrickyDeclarations that use type aliases that represent compound types and const
can yield surprising results. For example, the following declarations use the type pstring
, which is an alias for the the type char*
:
typedef char *pstring;
const pstring cstr = 0; // cstr is a constant pointer to char
const pstring *ps; // ps is a pointer to a constant pointer to char
The base type in these declarations is const pstring
. As usual, a const
that appears in the base type modifies the given type. The type of pstring
is “pointer to char
.” So, const pstring
is a constant pointer to char
—not a pointer to const char
.
It can be tempting, albeit incorrect, to interpret a declaration that uses a type alias by conceptually replacing the alias with its corresponding type:
const char *cstr = 0; // wrong interpretation of const pstring cstr
However, this interpretation is wrong. When we use pstring
in a declaration, the base type of the declaration is a pointer type. When we rewrite the declaration using char*
, the base type is char
and the *
is part of the declarator. In this case, const char
is the base type. This rewrite declares cstr
as a pointer to const char
rather than as a const
pointer to char
.
2.5.2. The auto
Type Specifier
FundamentalC++11It is not uncommon to want to store the value of an expression in a variable. To declare the variable, we have to know the type of that expression. When we write a program, it can be surprisingly difficult—and sometimes even impossible—to determine the type of an expression. Under the new standard, we can let the compiler figure out the type for us by using the auto
type specifier. Unlike type specifiers, such as double
, that name a specific type, auto
tells the compiler to deduce the type from the initializer. By implication, a variable that uses auto
as its type specifier must have an initializer:
// the type of item is deduced from the type of the result of adding val1 and val2
auto item = val1 + val2; // item initialized to the result of val1 + val2
Here the compiler will deduce the type of item
from the type returned by applying +
to val1
and val2
. If val1
and val2
are Sales_item
objects (§ 1.5, p. 19), item
will have type Sales_item
. If those variables are type double
, then item
has type double
, and so on.
As with any other type specifier, we can define multiple variables using auto
. Because a declaration can involve only a single base type, the initializers for all the variables in the declaration must have types that are consistent with each other:
auto i = 0, *p = &i; // ok: i is int and p is a pointer to int
auto sz = 0, pi = 3.14; // error: inconsistent types for sz and pi
Compound Types, const
, and auto
The type that the compiler infers for auto
is not always exactly the same as the initializer’s type. Instead, the compiler adjusts the type to conform to normal initialization rules.
First, as we’ve seen, when we use a reference, we are really using the object to which the reference refers. In particular, when we use a reference as an initializer, the initializer is the corresponding object. The compiler uses that object’s type for auto
’s type deduction:
int i = 0, &r = i;
auto a = r; // a is an int (r is an alias for i, which has type int)
Second, auto
ordinarily ignores top-level const
s (§ 2.4.3, p. 63). As usual in initializations, low-level const
s, such as when an initializer is a pointer to const
, are kept:
const int ci = i, &cr = ci;
auto b = ci; // b is an int (top-level const in ci is dropped)
auto c = cr; // c is an int (cr is an alias for ci whose const is top-level)
auto d = &i; // d is an int*(& of an int object is int*)
auto e = &ci; // e is const int*(& of a const object is low-level const)
If we want the deduced type to have a top-level const
, we must say so explicitly:
const auto f = ci; // deduced type of ci is int; f has type const int
We can also specify that we want a reference to the auto
-deduced type. Normal initialization rules still apply:
auto &g = ci; // g is a const int& that is bound to ci
auto &h = 42; // error: we can't bind a plain reference to a literal
const auto &j = 42; // ok: we can bind a const reference to a literal
When we ask for a reference to an auto
-deduced type, top-level const
s in the initializer are not ignored. As usual, const
s are not top-level when we bind a reference to an initializer.
When we define several variables in the same statement, it is important to remember that a reference or pointer is part of a particular declarator and not part of the base type for the declaration. As usual, the initializers must provide consistent auto
-deduced types:
auto k = ci, &l = i; // k is int; l is int&
auto &m = ci, *p = &ci; // m is a const int&;p is a pointer to const int
// error: type deduced from i is int; type deduced from &ci is const int
auto &n = i, *p2 = &ci;
INFO
Exercises Section 2.5.2
Exercise 2.33: Using the variable definitions from this section, determine what happens in each of these assignments:
a = 42; b = 42; c = 42;
d = 42; e = 42; g = 42;
Exercise 2.34: Write a program containing the variables and assignments from the previous exercise. Print the variables before and after the assignments to check whether your predictions in the previous exercise were correct. If not, study the examples until you can convince yourself you know what led you to the wrong conclusion.
Exercise 2.35: Determine the types deduced in each of the following definitions. Once you’ve figured out the types, write a program to see whether you were correct.
const int i = 42;
auto j = i; const auto &k = i; auto *p = &i;
const auto j2 = i, &k2 = i;
2.5.3. The decltype
Type Specifier
FundamentalSometimes we want to define a variable with a type that the compiler deduces from an expression but do not want to use that expression to initialize the variable. For such cases, the new standard introduced a second type specifier, decltype
, which returns the type of its operand. The compiler analyzes the expression to determine its type but does not evaluate the expression:
decltype(f()) sum = x; // sum has whatever type f returns
Here, the compiler does not call f
, but it uses the type that such a call would return as the type for sum
. That is, the compiler gives sum
the same type as the type that would be returned if we were to call f
.
The way decltype
handles top-level const
and references differs subtly from the way auto
does. When the expression to which we apply decltype
is a variable, decltype
returns the type of that variable, including top-level const
and references:
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x has type const int
decltype(cj) y = x; // y has type const int& and is bound to x
decltype(cj) z; // error: z is a reference and must be initialized
Because cj
is a reference, decltype(cj)
is a reference type. Like any other reference, z
must be initialized.
It is worth noting that decltype
is the only context in which a variable defined as a reference is not treated as a synonym for the object to which it refers.
decltype
and References
AdvancedWhen we apply decltype
to an expression that is not a variable, we get the type that that expression yields. As we’ll see in § 4.1.1 (p. 135), some expressions will cause decltype
to yield a reference type. Generally speaking, decltype
returns a reference type for expressions that yield objects that can stand on the left-hand side of the assignment:
// decltype of an expression can be a reference type
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // ok: addition yields an int; b is an (uninitialized) int
decltype(*p) c; // error: c is int& and must be initialized
Here r
is a reference, so decltype(r)
is a reference type. If we want the type to which r
refers, we can use r
in an expression, such as r + 0
, which is an expression that yields a value that has a nonreference type.
On the other hand, the dereference operator is an example of an expression for which decltype
returns a reference. As we’ve seen, when we dereference a pointer, we get the object to which the pointer points. Moreover, we can assign to that object. Thus, the type deduced by decltype(*p)
is int&
, not plain int
.
Another important difference between decltype
and auto
is that the deduction done by decltype
depends on the form of its given expression. What can be confusing is that enclosing the name of a variable in parentheses affects the type returned by decltype
. When we apply decltype
to a variable without any parentheses, we get the type of that variable. If we wrap the variable’s name in one or more sets of parentheses, the compiler will evaluate the operand as an expression. A variable is an expression that can be the left-hand side of an assignment. As a result, decltype
on such an expression yields a reference:
// decltype of a parenthesized variable is always a reference
decltype((i)) d; // error: d is int& and must be initialized
decltype(i) e; // ok: e is an (uninitialized) int
WARNING
Remember that decltype((
variable))
(note, double parentheses) is always a reference type, but decltype(
variable)
is a reference type only if variable is a reference.
INFO
Exercises Section 2.5.3
Exercise 2.36: In the following code, determine the type of each variable and the value each variable has when the code finishes:
int a = 3, b = 4;
decltype(a) c = a;
decltype((b)) d = a;
++c;
++d;
Exercise 2.37: Assignment is an example of an expression that yields a reference type. The type is a reference to the type of the left-hand operand. That is, if i
is an int
, then the type of the expression i = x
is int&
. Using that knowledge, determine the type and value of each variable in this code:
int a = 3, b = 4;
decltype(a) c = a;
decltype(a = b) d = a;
Exercise 2.38: Describe the differences in type deduction between decltype
and auto
. Give an example of an expression where auto
and decltype
will deduce the same type and an example where they will deduce differing types.