Lambda functions and expressions
Lambda函数及表达式
在标准C++语言中,尤其在使用诸如sort和find之类的标准库算法函数时,用户总是希望在算法函数调用的触发点附近定义谓词函数。在这一方面语言中只有一种机制可供利用:在函数中定义类。通常这种做法既啰嗦又笨重。另外,标准C++语言不允许在函数中定义的类充当模板参数,所以这种做法行不通。
显而易见,解决方案在于允许定义lambda表达式和 lambda函数。C++0x将允许定义lambda函数。
lambda函数可以定义如下:
[](int x, int y) { return x + y }
此无名函数的返回值类型为decltype(x+y)。只有lambda函数的形式为"return expression"时,返回值类型才能省略。因此这种lambda函数内部只能有一句语句。
返回值类型也可像下例那样明确指定。一个更为复杂的例子:
[](int x, int y) -> int { int z = x + y; return z + x; }
在此例中,临时变量z被创建并用于存储中间值。和普通函数一样,中间值在多次函数调用之间不会被保存。
如果lambda函数不返回值,即返回值类型为void的话,该返回值类型也可完全省略。
在lambda函数作用域范围内被定义的变量的引用也能被使用。这些变量的合集通常被称为闭包。闭包可以定义并使用如下:
std::vector<int> someList;
int total = 0;
std::for_each(someList.begin(), someList.end(), [&total](int x) {
total += x
});
std::cout << total;
这段代码将显示列表中所有元素的总和。变量total将被存为该lambda函数相应的闭包的一部分。由于闭包变量total是栈变量total的引用,使用前者可以改变后者的值。
为栈变量生成的闭包变量也可以不用引用操作符&定义,这种情况下lambda函数将拷贝其值。这种做法将促使用户明确声明其意图:是引用栈变量还是拷贝栈变量。引用栈变量可能会产生危险。如果某个lambda函数将在所创建的作用域之外被引用(比如将此lambda函数存放在std::function(C++0x标准)对象中可以做到这一点),那么用户必须保证该lambda函数没有引用任何栈变量。
对于那些可以保证只在其所创建的作用域内被执行的lambda函数,使用栈变量无须通过显式引用:
std::vector<int> someList;
int total = 0;
std::for_each(someList.begin(), someList.end(), [&](int x) {
total += x
});
这种lambda函数的具体内部实现可能会有所不同,但可以预期这种lambda函数可能不会保存所有栈变量的引用而是会保存函数创建时的栈指针。
如果不用[&]而用[=],那么所有被引用的变量都将被拷贝,从而允许lambda函数在原有变量生命期结束后仍然能够被使用。
缺省指示符还能和参数列表结合使用。比如,如果用户希望只拷贝其中一个变量的值,而对其他变量使用引用,则可使用下面的代码:
int total = 0;
int value = 5;
[&, value](int x) { total += (x * value) };
这段代码将导致total被存为引用,而value则会被存为拷贝。
如果一个lambda函数由一个类的某个成员函数定义,那么此lambda函数便被认定为该类的友元。这种lambda函数可以使用属于该类类型的对象的引用并访问其内部成员。
[](SomeType *typePtr) { typePtr->SomePrivateMemberFunction() };
只有当lambda函数在SomeType的某个成员函数中创建时这段代码才能工作。
对于指向当前成员函数所隶属对象的this指针,其处理有些特殊:必须在lambda函数中明确指定。
[this]() { this->SomePrivateMemberFunction() };
使用[&] 或 [=]形式将使this自动可用。
Lambda函数是一些类型取决于编译器的函数对象。它们的类型只对编译器开放。如果用户希望把lambda函数当作参数,那么要么参数相应类型为模板,要么创建一个std::function用于保存lambda函数。使用auto关键字则可以将lambda函数保存在局部变量中。
auto myLambdaFunc = [this]() { this->SomePrivateMemberFunction() };
然而,如果lambda函数的所有闭包变量均为引用,或者lambda函数根本没有闭包变量,那么所产生的函数对象将具有一种特殊类型:std::reference_closure<R(P)>。其中R(P)是带返回值的函数签名。这样做的理由在于期望此种类型的效率能好于使用std::function。
std::reference_closure<void()> myLambdaFunc = [this]() { this->SomePrivateMemberFunction() };
myLambdaFunc();
Unified function syntax
统一的函数语法
标准C语言的函数声明语法对于C语言的特性集来说是完美无缺的。由于C++语言演化自C语言,C++语言保留了相关的基本语法并在需要时进行扩充。然而,当C++变得更为复杂时,这种语法也暴露了一些局限性,尤其是在模板函数声明中。比如,以下代码在C++03中不合法:
template<typename LHS, typename RHS>
Ret // NOT VALID!
AddingFunc(const LHS &lhs, const RHS &rhs) {return lhs + rhs;}
类型Ret为任何LHS和RHS相加所产生的类型。即使有了前面所讲述的C++0x的decltype功能,仍然不行:
template<typename LHS, typename RHS>
decltype(lhs+rhs) // NOT VALID!
AddingFunc(const LHS &lhs, const RHS &rhs) {return lhs + rhs;}
这一段并非合法的C++0x代码,因为lhs和rhs尚未定义,只有在词法分析器分析出函数原型的其余部分之后这两者才能成为有效的标识符。
为解决这一问题,C++0x将引入一种新型的函数定义和声明的语法:
template<typename LHS, typename RHS>
[] // C++0x lambda introducer
AddingFunc(const LHS &lhs, const RHS &rhs) -> decltype(lhs+rhs) {return lhs + rhs;}
这一语法也能用于更为普通的函数声明和定义中:
struct SomeStruct
{
[]FuncName(int x, int y) -> int;
}
[]SomeStruct::FuncName(int x, int y) -> int
{
return x + y;
}
使用[]的语法与lambda函数完全相同。如果没有函数名,那么它就是一个lambda表达式。(由于lambda函数不能有模板,所以上面的例子不可能是lambda)既然函数可以在块区域中定义,用户也可以定义具名lambda函数:
[]Func1(int b)
{
[&]NamedLambda1(int a) -> int {return a + b;}
auto NamedLambda2 = [&](int a) {return a + b;} //lack of return type is legal for lambda specifications of this form.
}
这两句语句作用相同,即同样生成std::reference_closure<int(int)>类型的变量。但是,下例则有所不同:
[]Func1()
{
[] NamedLambda1 (int a) -> int {return a + 5;} // nested local function, valid in C++0X
auto NamedLambda2 = [](int a) {return a + 5;}
}
变量NamedLambda1是一个常规C式函数指针,就如同它被定义成int NamedLambda1(int a)那样。变量NamedLambda2则被定义成std::reference_closure<int(int)>类型。前一行展示了C++0x为嵌套函数所准备的新形式。
Concepts
约束
在C++语言中,模板类和模板函数必须对它们所接受的类型施加某些限制。比如,STL容器要求容器中的类型必须可以赋值。与类继承所展示的动多态(任何能接受Foo&类型对象作为参数的函数也能传入Foo的子类型)有所不同,任何类只要支持某个模板所使用的操作,它就能被用于该模板。在函数传参数的情况下,参数所必须满足的需求是清晰的(必须是Foo的子类型),而模板的场合下,对象所需满足的接口则是隐含在模板实现当中的。约束则提供了一种将模板参数所必需满足的接口代码化的机制。
引入约束的最初动因在于改进编译错误信息的质量。如果程序员试图使用一种不能提供某个模板所需接口的类型,那么编译器将产生错误信息。然而,这些错误信息通常难以理解,尤其对于新手而言。首先,错误信息中的模板参数通常被完整拼写出来,这将导致异常庞大的错误信息。在某些编译器上,简单的错误会产生好几K的错误信息。其次,这些错误信息通常不会指向错误的实际发生地点。比如,如果程序员试图创建一个其成员为不具备拷贝构造器对象的vector,首先出现的错误信息几乎总是指向vector类中试图拷贝构造其成员的那段代码。程序员必须具备足够的经验和能力才能判断出实际的错误在于相应类型无法完全满足vector所需要的接口。
在试图解决此问题的过程中,C++0x为语言添加了约束这一特性。与OOP使用基类来限制类型的功能相似,约束是一种限制类型接口的具名结构。而与OOP所不同的是,约束定义并非总是与传入模板的参数类型明确相关,但它总是与模板定义相关:
template<LessThanComparable T>
const T& min(const T &x, const T &y)
{
return y < x ? y : x;
}
这里没有用class 或 typename将模板参数指定为任意类型,而是使用了LessThanComparable这个之前定义的约束。如果某个传入min模板参数的类型不符合LessThanComparable约束的定义,那么编译器将报告编译错误,告诉用户用来具现化该模板的类型不符合LessThanComparable约束。
下面是一个更一般化的约束形式:
template<typename T> requires LessThanComparable<T>
const T& min(const T &x, const T &y)
{
return y < x ? y : x;
}
关键字requires之后为一串约束的声明。它可以被用于表述涉及多个类型的约束。此外,如果用户希望当类型匹配该约束时不要使用某个特定模板,也可以用requires !LessThanComparable<T>。可以像模板特化那样使用这种机制。一个通用模板可能通过显式禁用一些特性丰富的约束来处理具有较少特性的类型。而这些约束则可通过特化利用某些特性来取得更高的效率并实现更多的功能。
约束定义如下:
auto concept LessThanComparable<typename T>
{
bool operator<(T, T);
}
这个例子中的关键字auto意味着任何类型只要支持约束中所指定的操作便被认定支持该约束。如果不使用auto关键字,为声明某个类型支持该约束就必须对该类型使用约束映射。
该约束声明任何类型只要定义了接受两个参数并返回bool型的<操作符就被认为是LessThanComparable。该操作符不一定是一个自由函数,它也可以是T类型的成员函数。
约束也可以涉及多个类型。比如,约束能表示一个类型可以转换为另一个类型:
auto concept Convertible<typename T, typename U>
{
operator U(const T&);
}
为了在模板中使用这个约束,模板必须使用一种更为一般化的形式:
template<typename U, typename T> requires Convertible<T, U>
U convert(const T& t)
{
return t;
}
约束可以组合运用。比如,给定一个名为Regular的约束
concept InputIterator<typename Iter, typename Value>
{
requires Regular<Iter>;
Value operator*(const Iter&);
Iter& operator++(Iter&);
Iter operator++(Iter&, int);
}
InputIterator约束的第一个模板参数必须符合Regular约束。
与继承相似,约束也可派生自另一约束。与类继承相似,满足派生约束所有限制条件的类型必须满足基本约束的所有限制条件。约束派生定义形同类派生:
concept ForwardIterator<typename Iter, typename Value> : InputIterator<Iter, Value>
{
//Add other requirements here.
}
类型名可以与约束相关。这将施加一些限制条件:在使用这些约束的模板中,这些类型名可供使用:
concept InputIterator<typename Iter>
{
typename value_type;
typename reference;
typename pointer;
typename difference_type;
requires Regular<Iter>;
requires Convertible<reference, value_type>;
reference operator*(const Iter&); // dereference
Iter& operator++(Iter&); // pre-increment
Iter operator++(Iter&, int); // post-increment
// ...
}
约束映射允许某些类型被显式绑定到某个约束。如有可能,约束映射也允许在不改变类型定义的前提下让该类型采用某个约束的语法。比如下例:
concept_map InputIterator<char*>
{
typedef char value_type ;
typedef char& reference ;
typedef char* pointer ;
typedef std::ptrdiff_t difference_type ;
};
这个约束映射填补了当InputIterator映射作用于char*类型时所需要的类型名。
为增加灵活性,约束映射本身也能被模板化。上例可以被延伸至所有指针类型:
template<typename T> concept_map InputIterator<T*>
{
typedef T value_type ;
typedef T& reference ;
typedef T* pointer ;
typedef std::ptrdiff_t difference_type ;
};
此外,约束映射也可充当迷你类型,此时它会包含函数定义以及其他与类相关的结构设施:
concept Stack<typename X>
{
typename value_type;
void push(X&, const value_type&);
void pop(X&);
value_type top(const X&);
bool empty(const X&);
};
template<typename T> concept_map Stack<std::vector<T> >
{
typedef T value_type;
void push(std::vector<T>& v, const T& x) { v.push_back(x); }
void pop(std::vector<T>& v) { v.pop_back(); }
T top(const std::vector<T>& v) { return v.back(); }
bool empty(const std::vector<T>& v) { return v.empty(); }
};
这个约束映射将允许任何接受实现了Stack约束的类型的模板也接受std::vector,同时将所有函数调用映射为对std::vector的调用。最终,这种做法将允许一个已经存在的对象在不改变其定义的前提下,转换其接口并为模板函数所利用。
最后需要指出的是,某些限制条件可以通过静态断言来检测。这种手段可以用来检测那些模板需要但却面向其他方面问题的限制条件。