Chapter 2.5-2.6
Chapter 2.5 introduces more facilities that can deal with types. They are required to obey the rules that applied to built-in types and compound types we have learned. Chapter 2.6 introduces how can we write a safe header file for our data structure.
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.
The first way to define a type alias is using a keyword typedef:
The keyword typedef may appear as part of the base type of a declaration. Declarations that include typedef define type aliases rather than variables.
typedef double wages;
typedef wage base, *p; // base is a double, p is a double *
The second way to define alias is using alias declaration:
An alias declaration starts with the keyword using followed by the alias name and an =.
using SI = Sales_item; // SI is a synonym for Sales_item
It can be tempting, albeit incorrect, to interpret a declaration that use a type alias by conceptually replacing the alias with its corresponding type.
For example,
typedef char *pstring;
const pstring cstr = 0; // cstr is a const pointer
const pstring *ps; // ps is pointer to a const pointer to char
For the second line, if we just conceptually replace “pstring” by “char *”, which makes the declaration looks like
const char *cstr = 0;
According to this incorrect understanding, cstr becomes a pointer to const, and “const char” becomes the base type of this declaration.
According to the C++ rules, we should take the alias name as a single type name. In this way, the
“const” is used to modify “pstring”, and the base type of this declaration is “pstring”. As a result, cstr is a const pointer.
we can let the compiler figure out the type for us by using the auto specifier.
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.
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.
a reference or a printer 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.
For example,
auto sz = 0, pi = 3.14;
int i = 0;
const int ci = i;
auto &n = i, *p2 = &ci;
In the first line, the first initializer is int, but the second initializer is double or float, which is not consistent with the first one. It would lead to an error.
In the fourth line, the first initializer is int, but the seconde initializer is const int, which will cause an error too.
There are two rules need to be careful when we use auto. The first one is
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.
The second rule is
auto ordinarily ignores top-level consts. As usual in initializations, low-level consts, such as when an initializer is a pointer to const, are kept.
For example,
int i = 0, &r = i;
const int ci = i, &cr = ci;
auto c = cr;
In the third line, cr is a reference, but in this auto declaration, the object that cr refers to is used, that is a const int. This const is a top-level const, so the compiler would give the int type to variable c.
Sometimes 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 case, 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.
That is to say, no matter what expression is written in the decltype declaration, the expression won’t be calculated or executed.
There are a few matters that should be noticed when using decltype:
It worth noting that decltype is the only context in which a variable defined as a reference it not treated as a synonym for the object to which it refers.
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
the dereference operator is an example of an expression for which decltype returns a reference.
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 (that yield objects that can stand on the left-hand side of the assignment). As a result, decltype on such an expression yields a reference.
For example,
int i = 42, *p = &i, &r = i;
decltype(r+0) b; // int
decltype(*p) c = &i; // int&
decltype((i)) d = &i; // int&
decltype(i) d; // int
In the second line, r is a reference, so decltype® is a reference type. However, the result of the expression r+0 is an int. We can give variable b the same type as the object that r refers. r+0 is not an expression that yields an object that can stand on the left-hand side of the assignment. So decltype(r+0) won’t give variable b the reference type.
In the third line, “*p” is an expression that yield objects that can stand on the left-hand side of the assignment. Therefore, decltype(*p) would give variable c the reference type.
In the fourth line, “(i)” is an expression that yields an object that can stand on the left-hand side of the assignment, not a single variable, in this case, an lvalue expression, whose value is identical to the value of the variable i. (the definition of lvalue will be introduced in later chapters) Therefore, decltype((i)) gives variable d the reference type.
It should be noted that, in a decltype declaration, we have to give initializers to those which are definitly reference. This is a rule that all kinds of reference have to obey.
The functions of decltype and auto are similar. Both of them make the compiler deduce the type of a variable.
However, there are some differences between them
auto | decltype |
---|---|
ignore the top-level const. when an initializer is a reference, the object that the reference refers to decide the type | decltype returns the type of that variable, including top-level const and references. |
the expression in the initializer would be calculated when compiling | the expression given to decltype woun’t be calculated or executed when compiling |
the expression is used as the initializer | the expression is used in a pair of parantheses which looks like the parameter of the decltype |
initializer is a must | initializer is not a must |
programs that use Sales_data will include the string header twice: once directly and once as a side effect of including Sales_data.h. Because a header might be included more than once, we need to write our headers in a way that is safe even if the header is included multiple times.
I believe that many rules in C++ are created due to different kinds of problems. The rules are reasonable.
Here is the solution to headers being included multiple times: head guards, which relies on the preprocessor.
The preprocessor–which C++ inherits from C–is a program that runs before the compiler and changes the source text of our programs.
When the preprocessor sees a #include, it replaces the #include with the contents of the specified header.
That is to say, after preprocessing, all the #include in the program will be replaced by the contents in the .h file. During this procedure, the head guards in the .h files work.
Head guards rely on preprocessor variables. Preprocessor variables have one or two possible states: defined or not defined.
The #define directive takes a name and defines that name as a preprocessor variable.
There are two other directives that test whether a given preprocessor variable has or has not been defined: #ifdef and #ifndef. If the test is true, then everything following the #ifndef is processed up to the matching #endif.
For example,
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
The name of the preprocess variable is usually the uppercase version of the .h file name. It is recommended that every .h file has a header guard.
type | reason |
---|---|
all references | we cannot rebind a reference to the second variable |
all const | the value of all const variables cannot be changed |
variable that uses auto as its type specifier | the compiler have to deduce the type from the initializer |
No matter in copying or in initialization, the most important is :
low-level const matters more than top-level const.
In copying between two variables with different types, the top-level const can be ignored, but the low-level const decide whether this copying or conversion is legal or not.
In the initialization of an auto type, the top-level const of the initializer can be ignored, which means the top-level const feature won’t be passed to the new variable. Instead, the low-level const feature would be kept in the new variable.
type alias 类型别名
type specifier 类说明符
preprocessor 预处理器
head guard 头文件保护符