B. Stroustrup - The C++ Programming Language (794319), страница 42
Текст из файла (страница 42)
Also, it is more resilientto code changes. For example, if I changed arg to be a list, the loop using auto would still work correctly whereas the first loop would need to be rewritten. So, unless there is a good reason not to,use auto in small scopes.If a scope is large, mentioning a type explicitly can help localize errors. That is, compared tousing a specific type, using auto can delay the detection of type errors. For example:void f(double d){constexpr auto max = d+7;int a[max];// error: array bound not an integer// ...}If auto causes surprises, the best cure is typically to make functions smaller, which most often is agood idea anyway (§12.1).164Types and DeclarationsChapter 6We can decorate a deduced type with specifiers and modifiers (§6.3.1), such as const and & (reference; §7.7).
For example:void f(vector<int>& v){for (const auto& x : v) {// ...}}// x is a const int&Here, auto is determined by the element type of v, that is, int.Note that the type of an expression is never a reference because references are implicitly dereferenced in expressions (§7.7). For example:void g(int& v){auto x = v;auto& y = v;}// x is an int (not an int&)// y is an int&6.3.6.2 auto and {}-listsWhen we explicitly mention the type of an object we are initializing, we have two types to consider: the type of the object and the type of the initializer. For example:char v1 = 12345;int v2 = 'c';T v3 = f();// 12345 is an int// 'c' is a charBy using the {}-initializer syntax for such definitions, we minimize the chances for unfortunate conversions:char v1 {12345};int v2 {'c'};T v3 {f()};// error : narrowing// fine: implicit char->int conversion// works if and only if the type of f() can be implicitly converted to a TWhen we use auto, there is only one type involved, the type of the initializer, and we can safely usethe = syntax:auto v1 = 12345;auto v2 = 'c';auto v3 = f();// v1 is an int// v2 is a char// v3 is of some appropriate typeIn fact, it can be an advantage to use theprise someone:auto v1 {12345};auto v2 {'c'};auto v3 {f()};=syntax with// v1 is a list of int// v2 is a list of char// v3 is a list of some appropriate typeThis is logical.
Consider:auto,because the {}-list syntax might sur-Section 6.3.6.2auto x0 {};auto x1 {1};auto x2 {1,2};auto x3 {1,2,3};autoand {}-lists165// error: cannot deduce a type// list of int with one element// list of int with two elements// list of int with three elementsThe type of a homogeneous list of elements of type T is taken to be of type initializer_list<T>(§3.2.1.3, §11.3.3). In particular, the type of x1 is not deduced to be int. Had it been, what wouldbe the types of x2 and x3?Consequently, I recommend using = rather than {} for objects specified auto whenever we don’tmean ‘‘list.’’6.3.6.3 The decltype() SpecifierWe can use auto when we have a suitable initializer.
But sometimes, we want to have a typededuced without defining an initialized variable. Then, we can use a declaration type specifier:decltype(expr) is the declared type of expr. This is mostly useful in generic programming. Considerwriting a function that adds two matrices with potentially different element types. What should bethe type of the result of the addition? A matrix, of course, but what might its element type be? Theobvious answer is that the element type of the sum is the type of the sum of the elements. So, I candeclare:template<class T, class U>auto operator+(const Matrix<T>& a, const Matrix<U>& b) −> Matrix<decltype(T{}+U{})>;I use the suffix return type syntax (§12.1) to be able to express the return type in terms of the arguments: Matrix<decltype(T{}+U{})>.
That is, the result is a Matrix with the element type being whatyou get from adding a pair of elements from the argument Matrixes: T{}+U{}.In the definition, I again need decltype() to express Matrix’s element type:template<class T, class U>auto operator+(const Matrix<T>& a, const Matrix<U>& b) −> Matrix<decltype(T{}+U{})>{Matrix<decltype(T{}+U{})> res;for (int i=0; i!=a.rows(); ++i)for (int j=0; j!=a.cols(); ++j)res(i,j) += a(i,j) + b(i,j);return res;}6.4 Objects and ValuesWe can allocate and use objects that do not have names (e.g., created using new), and it is possibleto assign to strange-looking expressions (e.g., ∗p[a+10]=7).
Consequently, we need a name for‘‘something in memory.’’ This is the simplest and most fundamental notion of an object. That is,an object is a contiguous region of storage; an lvalue is an expression that refers to an object. Theword ‘‘lvalue’’ was originally coined to mean ‘‘something that can be on the left-hand side of anassignment.’’ However, not every lvalue may be used on the left-hand side of an assignment; an166Types and DeclarationsChapter 6lvalue can refer to a constant (§7.7).
An lvalue that has not been declared const is often called amodifiable lvalue. This simple and low-level notion of an object should not be confused with thenotions of class object and object of polymorphic type (§3.2.2, §20.3.2).6.4.1 Lvalues and RvaluesTo complement the notion of an lvalue, we have the notion of an rvalue. Roughly, rvalue means ‘‘avalue that is not an lvalue,’’ such as a temporary value (e.g., the value returned by a function).If you need to be more technical (say, because you want to read the ISO C++ standard), youneed a more refined view of lvalue and rvalue. There are two properties that matter for an objectwhen it comes to addressing, copying, and moving:• Has identity: The program has the name of, pointer to, or reference to the object so that it ispossible to determine if two objects are the same, whether the value of the object haschanged, etc.• Movable: The object may be moved from (i.e., we are allowed to move its value to anotherlocation and leave the object in a valid but unspecified state, rather than copying; §17.5).It turns out that three of the four possible combinations of those two properties are needed to precisely describe the C++ language rules (we have no need for objects that do not have identity andcannot be moved).
Using ‘‘m for movable’’ and ‘‘i for has identity,’’ we can represent this classification of expressions graphically:lvalue {i&!m}xvalue {i&m}glvalue {i}prvalue {!i&m}rvalue {m}So, a classical lvalue is something that has identity and cannot be moved (because we could examine it after a move), and a classical rvalue is anything that we are allowed to move from. The otheralternatives are prvalue (‘‘pure rvalue’’), glvalue (‘‘generalized lvalue’’), and xvalue (‘‘x’’ for ‘‘extraordinary’’ or ‘‘expert only’’; the suggestions for the meaning of this ‘‘x’’ have been quite imaginative).
For example:void f(vector<string>& vs){vector<string>& v2 = std::move(vs);// ...}// move vs to v2Here, std::move(vs) is an xvalue: it clearly has identity (we can refer to it as vs), but we have explicitly given permission for it to be moved from by calling std::move() (§3.3.2, §35.5.1).For practical programming, thinking in terms of rvalue and lvalue is usually sufficient. Notethat every expression is either an lvalue or an rvalue, but not both.6.4.2 Lifetimes of ObjectsThe lifetime of an object starts when its constructor completes and ends when its destructor startsexecuting.
Objects of types without a declared constructor, such as an int, can be considered tohave default constructors and destructors that do nothing.Section 6.4.2Lifetimes of Objects167We can classify objects based on their lifetimes:• Automatic: Unless the programmer specifies otherwise (§12.1.8, §16.2.12), an objectdeclared in a function is created when its definition is encountered and destroyed when itsname goes out of scope. Such objects are sometimes called automatic objects. In a typicalimplementation, automatic objects are allocated on the stack; each call of the function getsits own stack frame to hold its automatic objects.• Static: Objects declared in global or namespace scope (§6.3.4) and statics declared in functions (§12.1.8) or classes (§16.2.12) are created and initialized once (only) and ‘‘live’’ untilthe program terminates (§15.4.3).
Such objects are called static objects. A static object hasthe same address throughout the life of a program execution. Static objects can cause serious problems in a multi-threaded program because they are shared among all threads andtypically require locking to avoid data races (§5.3.1, §42.3).• Free store: Using the new and delete operators, we can create objects whose lifetimes arecontrolled directly (§11.2).• Temporary objects (e.g., intermediate results in a computation or an object used to hold avalue for a reference to const argument): their lifetime is determined by their use. If theyare bound to a reference, their lifetime is that of the reference; otherwise, they ‘‘live’’ untilthe end of the full expression of which they are part.
A full expression is an expression thatis not part of another expression. Typically, temporary objects are automatic.• Thread-local objects; that is, objects declared thread_local (§42.2.8): such objects are created when their thread is and destroyed when their thread is.Static and automatic are traditionally referred to as storage classes.Array elements and nonstatic class members have their lifetimes determined by the object ofwhich they are part.6.5 Type AliasesSometimes, we need a new name for a type. Possible reasons include:• The original name is too long, complicated, or ugly (in some programmer’s eyes).• A programming technique requires different types to have the same name in a context.• A specific type is mentioned in one place only to simplify maintenance.For example:using Pchar = char∗;using PF = int(∗)(double);// pointer to character// pointer to function taking a double and returning an intSimilar types can define the same name as a member alias:template<class T>class vector {using value_type = T;// ...};// every container has a value_type168Types and Declarationstemplate<class T>class list {using value_type = T;// ...};Chapter 6// every container has a value_typeFor good and bad, type aliases are synonyms for other types rather than distinct types.