B. Stroustrup - The C++ Programming Language (794319), страница 66
Текст из файла (страница 66)
However,beware that zero elements can be a special case. If so, that case should be handled by a default constructor (§17.3.3).The type of a {}-list can be deduced (only) if all elements are of the same type. For example:auto x0 = {};auto x1 = {1};auto x2 = {1,2};auto x3 = {1,2,3};auto x4 = {1,2.0};// error (no element type)// initializer_list<int>// initializer_list<int>// initializer_list<int>// error : nonhomogeneous listUnfortunately, we do not deduce the type of an unqualified list for a plain template argument.
Forexample:template<typename T>void f(T);f({});f({1});f({1,2});f({1,2,3});// error: type of initializer is unknown// error: an unqualified list does not match ‘‘plain T’’// error: an unqualified list does not match ‘‘plain T’’// error: an unqualified list does not match ‘‘plain T’’I say ‘‘unfortunately’’ because this is a language restriction, rather than a fundamental rule.
Itwould be technically possible to deduce the type of those {}-lists as initializer_list<int>, just like wedo for auto initializers.Similarly, we do not deduce the element type of a container represented as a template. Forexample:template<class T>void f2(const vector<T>&);f2({1,2,3});f2({"Kona","Sidney"});// error : cannot deduce T// error : cannot deduce T290Select OperationsChapter 11This too is unfortunate, but it is a bit more understandable from a language-technical point of view:nowhere in those calls does it say vector.
To deduce T the compiler would first have to decide thatthe user really wanted a vector and then look into the definition of vector to see if it has a constructor that accepts {1,2,3}. In general, that would require an instantiation of vector (§26.2). It would bepossible to handle that, but it could be costly in compile time, and the opportunities for ambiguitiesand confusion if there were many overloaded versions of f2() are reasons for caution. To call f2(), bemore specific:f2(vector<int>{1,2,3});f2(vector<string>{"Kona","Sidney"});// OK// OK11.4 Lambda ExpressionsA lambda expression, sometimes also referred to as a lambda function or (strictly speaking incorrectly, but colloquially) as a lambda, is a simplified notation for defining and using an anonymousfunction object.
Instead of defining a named class with an operator(), later making an object of thatclass, and finally invoking it, we can use a shorthand. This is particularly useful when we want topass an operation as an argument to an algorithm. In the context of graphical user interfaces (andelsewhere), such operations are often referred to as callbacks. This section focuses on technicalaspects of lambdas; examples and techniques for the use of lambdas can be found elsewhere(§3.4.3, §32.4, §33.5.2).A lambda expression consists of a sequence of parts:• A possibly empty capture list, specifying what names from the definition environment canbe used in the lambda expression’s body, and whether those are copied or accessed by reference.
The capture list is delimited by [] (§11.4.3).• An optional parameter list, specifying what arguments the lambda expression requires. Theparameter list is delimited by () (§11.4.4).• An optional mutable specifier, indicating that the lambda expression’s body may modify thestate of the lambda (i.e., change the lambda’s copies of variables captured by value)(§11.4.3.4).• An optional noexcept specifier.• An optional return type declaration of the form −> type (§11.4.4).• A body, specifying the code to be executed. The body is delimited by {} (§11.4.3).The details of passing arguments, returning results, and specifying the body are those of functionsand are presented in Chapter 12.
The notion of ‘‘capture’’ of local variables is not provided forfunctions. This implies that a lambda can act as a local function even though a function cannot.11.4.1 Implementation ModelLambda expressions can be implemented in a variety of ways, and there are some rather effectiveways of optimizing them. However, I find it useful to understand the semantics of a lambda byconsidering it a shorthand for defining and using a function object.
Consider a relatively simpleexample:Section 11.4.1Implementation Model291void print_modulo(const vector<int>& v, ostream& os, int m)// output v[i] to os if v[i]%m==0{for_each(begin(v),end(v),[&os,m](int x) { if (x%m==0) os << x << '\n'; });}To see what this means, we can define the equivalent function object:class Modulo_print {ostream& os; // members to hold the capture listint m;public:Modulo_print(ostream& s, int mm) :os(s), m(mm) {}void operator()(int x) const{ if (x%m==0) os << x << '\n'; }};// captureThe capture list, [&os,m], becomes two member variables and a constructor to initialize them. The& before os means that we should store a reference, and the absence of a & for m means that weshould store a copy.
This use of & mirrors its use in function argument declarations.The body of the lambda simply becomes the body of the operator()(). Since the lambda doesn’treturn a value, the operator()() is void. By default, operator()() is const, so that the lambda bodydoesn’t modify the captured variables. That’s by far the most common case. Should you want tomodify the state of a lambda from its body, the lambda can be declared mutable (§11.4.3.4). Thiscorresponds to an operator()() not being declared const.An object of a class generated from a lambda is called a closure object (or simply a closure).We can now write the original function like this:void print_modulo(const vector<int>& v, ostream& os, int m)// output v[i] to os if v[i]%m==0{for_each(begin(v),end(v),Modulo_print{os,m});}If a lambda potentially captures every local variable by reference (using the capture listclosure may be optimized to simply contain a pointer to the enclosing stack frame.[&]),the11.4.2 Alternatives to LambdasThat final version of print_modulo() is actually quite attractive, and naming nontrivial operations isgenerally a good idea.
A separately defined class also leaves more room for comments than does alambda embedded in some argument list.However, many lambdas are small and used only once. For such uses, the realistic equivalentinvolves a local class defined immediately before its (only) use. For example:292Select Operationsvoid print_modulo(const vector<int>& v, ostream& os, int m)// output v[i] to os if v[i]%m==0{class Modulo_print {ostream& os; // members to hold the capture listint m;public:Modulo_print (ostream& s, int mm) :os(s), m(mm) {}void operator()(int x) const{ if (x%m==0) os << x << '\n'; }};Chapter 11// capturefor_each(begin(v),end(v),Modulo_print{os,m});}Compared to that, the version using the lambda is a clear winner. If we really want a name, we canjust name the lambda:void print_modulo(const vector<int>& v, ostream& os, int m)// output v[i] to os if v[i]%m==0{auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };for_each(begin(v),end(v),Modulo_print);}Naming the lambda is often a good idea.
Doing so forces us to consider the design of the operationa bit more carefully. It also simplifies code layout and allows for recursion (§11.4.5).Writing a for-loop is an alternative to using a lambda with a for_each(). Consider:void print_modulo(const vector<int>& v, ostream& os, int m)// output v[i] to os if v[i]%m==0{for (auto x : v)if (x%m==0) os << x << '\n';}Many would find this version much clearer than any of the lambda versions.
However, for_each is arather special algorithm, and vector<int> is a very specific container. Consider generalizingprint_modulo() to handle arbitrary containers:template<class C>void print_modulo(const C& v, ostream& os, int m)// output v[i] to os if v[i]%m==0{for (auto x : v)if (x%m==0) os << x << '\n';}This version works nicely for a map. The C++ range-for-statement specifically caters to the specialcase of traversing a sequence from its beginning to its end. The STL containers make suchSection 11.4.2Alternatives to Lambdas293traversals easy and general. For example, using a for-statement to traverse a map gives a depth-firsttraversal. How would we do a breadth-first traversal? The for-loop version of print_modulo() is notamenable to change, so we have to rewrite it to an algorithm.
For example:template<class C>void print_modulo(const C& v, ostream& os, int m)// output v[i] to os if v[i]%m==0{breadth_first(begin(v),end(v),[&os,m](int x) { if (x%m==0) os << x << '\n'; });}Thus, a lambda can be used as ‘‘the body’’ for a generalized loop/traversal construct represented asan algorithm. Using for_each rather than breadth_first would give depth-first traversal.The performance of a lambda as an argument to a traversal algorithm is equivalent (typicallyidentical) to that of the equivalent loop. I have found that to be quite consistent across implementations and platforms.
The implication is that we have to base our choice between ‘‘algorithm pluslambda’’ and ‘‘for-statement with body’’ on stylistic grounds and on estimates of extensibility andmaintainability.11.4.3 CaptureThe main use of lambdas is for specifying code to be passed as arguments. Lambdas allow that tobe done ‘‘inline’’ without having to name a function (or function object) and use it elsewhere.Some lambdas require no access to their local environment.
Such lambdas are defined with theempty lambda introducer []. For example:void algo(vector<int>& v){sort(v.begin(),v.end());// sor t values// ...sort(v.begin(),v.end(),[](int x, int y) { return abs(x)<abs(y); });// ...}// sor t absolute valuesIf we want to access local names, we have to say so or get an error:void f(vector<int>& v){bool sensitive = true;// ...sort(v.begin(),v.end(),[](int x, int y) { return sensitive ? x<y : abs(x)<abs(y); });}// error : can’t access sensitiveI used the lambda introducer [].
This is the simplest lambda introducer and does not allow thelambda to refer to names in the calling environment. The first character of a lambda expression isalways [. A lambda introducer can take various forms:294Select OperationsChapter 11•[]: an empty capture list. This implies that no local names from the surrounding context canbe used in the lambda body. For such lambda expressions, data is obtained from argumentsor from nonlocal variables.• [&]: implicitly capture by reference. All local names can be used. All local variables areaccessed by reference.• [=]: implicitly capture by value. All local names can be used.