经典的 C++ 编程语言在 1998 年实现了标准化,随后在 2003 年进行了小规模的修订(大部分是修正)。为了支持高级抽象,开发人员依赖于 Boost(http://www.boost.org)库和其他公共领域库。由于下一波标准化浪潮,该语言(从 C++ 11 开始)得到了增强,现在开发人员可以对最广泛使用的抽象进行编码(由其他语言支持),而无需依赖外部库。甚至线程和文件系统接口,这些都在库的保护之下,现在也是标准语言的一部分。现代 C++(代表 C++ 版本 11/14/17)包含对该语言及其库的卓越补充,这使得 C++ 成为编写工业力量生产软件的事实上的选择。本章涵盖的特性是程序员使用反应式编程结构(尤其是 RxCpp)必须了解的最基本的特性。本章的主要目的是介绍该语言中最重要的补充内容,这些内容使得实现反应式编程结构变得更加容易,而无需求助于深奥的语言技术。诸如 Lambda 函数、自动类型推断、右值引用、移动语义和语言级并发等构造是本书作者认为每个 C++ 程序员都应该知道的一些构造。在本章中,我们将涵盖以下主题:
- C++ 编程语言设计的关键问题
- 为了编写更好的代码,对 C++ 进行了一些增强
- 通过右值引用和移动语义实现更好的内存管理
- 使用一组增强的智能指针实现更好的对象生存期管理
- 使用 Lambda 函数和表达式的行为参数化
- 函数包装器(
std::function
类型) - 其他功能
- 编写迭代器和观察器(将所有东西放在一起)
就开发人员而言,C++ 编程语言设计人员牢记的三个关键问题是(现在仍然是):
- 零成本抽象-对更高级别的抽象没有性能损失
- 表现性-用户定义的类型 ( UDT )或类应该像内置类型一样表现性
- 可替代性-UDT 可以在任何期望内置类型的地方被替代(如在通用数据结构和算法中)
我们将简要讨论这些。
C++ 编程语言一直帮助开发人员编写利用微处理器(生成的代码在其上运行)的代码,并在重要时提高抽象级别。在提升抽象的同时,语言的设计者总是试图最小化(几乎消除)他们的性能开销。这被称为零成本抽象或零间接成本抽象。唯一值得注意的损失是分派虚函数时的间接调用(通过函数指针)成本。尽管语言增加了大量的特性,设计者从一开始就保持了语言隐含的“零成本抽象”保证。
C++ 帮助开发人员编写用户定义的类型或类,它们可以像编程语言的内置类型一样具有表现力。这使得人们能够编写任意精度的算术类(在某些语言中被称为BigInteger
/ BigFloat
,它包含了双精度或浮点的所有特性。为了便于解释,我们定义了一个包装 IEEE 双精度浮点数的SmartFloat
类,双数据类型可用的大多数运算符都是重载的。下面的代码片段显示,可以编写模仿内置类型(如 int、float 或 double)语义的类型:
//---- SmartFloat.cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class SmartFloat {
double _value; // underlying store
public:
SmartFloat(double value) : _value(value) {}
SmartFloat() : _value(0) {}
SmartFloat( const SmartFloat& other ) { _value = other._value; }
SmartFloat& operator = ( const SmartFloat& other ) {
if ( this != &other ) { _value = other._value;}
return *this;
}
SmartFloat& operator = (double value )
{ _value = value; return *this;}
~SmartFloat(){ }
SmartFloat
类包装了一个双精度值,并定义了一些构造函数和赋值运算符来正确初始化实例。在下面的代码片段中,我们将定义一些有助于增加值的运算符。定义了运算符的前缀和后缀变体:
SmartFloat& operator ++ () { _value++ ; return *this; }
SmartFloat operator ++ (int) { // postfix operator
SmartFloat nu(*this); ++ _value; return nu;
}
SmartFloat& operator -- () { _value--; return *this; }
SmartFloat operator -- (int) {
SmartFloat nu(*this); --_value; return nu;
}
前面的代码片段实现了增量运算符(前缀和后缀),仅用于演示目的。在现实世界的类中,我们将检查浮点上溢和下溢,以使代码更加健壮。包装一个类型的全部目的是为了编写健壮的代码!
SmartFloat& operator += ( double x ) { _value += x; return *this;}
SmartFloat& operator -= ( double x ) { _value -= x;return *this; }
SmartFloat& operator *= ( double x ) { _value *= x; return *this;}
SmartFloat& operator /= ( double x ) { _value /= x; return *this;}
前面的代码片段实现了 C++ 风格的赋值操作符,同样,为了简短起见,我们没有检查是否有任何浮点上溢或下溢。我们在这里也不处理异常,以保持列表的简短。
bool operator > ( const SmartFloat& other )
{ return _value > other._value; }
bool operator < ( const SmartFloat& other )
{return _value < other._value;}
bool operator == ( const SmartFloat& other )
{ return _value == other._value;}
bool operator != ( const SmartFloat& other )
{ return _value != other._value;}
bool operator >= ( const SmartFloat& other )
{ return _value >= other._value;}
bool operator <= ( const SmartFloat& other )
{ return _value <= other._value;}
前面的代码实现了关系运算符,与双精度浮点相关联的大多数语义都已实现,如下所示:
operator int () { return _value; }
operator double () { return _value;}
};
为了完整起见,我们实现了对int
和double
的转换运算符。我们将编写两个函数来聚合存储在数组中的值。第一个函数需要一个double
数组作为参数,第二个函数需要一个SmartFloat
数组作为参数。两个例程中的代码是相同的,只有类型发生了变化。两者将产生相同的结果:
double Accumulate( double a[] , int count ){
double value = 0;
for( int i=0; i<count; ++ i) { value += a[i]; }
return value;
}
double Accumulate( SmartFloat a[] , int count ){
SmartFloat value = 0;
for( int i=0; i<count; ++ i) { value += a[i]; }
return value;
}
int main() {
// using C++ 1z's initializer list
double x[] = { 10.0,20.0,30,40 };
SmartFloat y[] = { 10,20.0,30,40 };
double res = Accumulate(x,4); // will call the double version
cout << res << endl;
res = Accumulate(y,4); // will call the SmartFloat version
cout << res << endl;
}
C++ 语言帮助我们编写扩展基本类型语义的表达类型。语言的表现力也有助于使用语言支持的多种技术编写好的值类型和引用类型。由于支持操作符重载、转换操作符、布局新以及其他相关技术,与同时代的其他语言相比,该语言将类设计提升到了一个更高的水平。但是,伴随着权力而来的是责任,语言有时会给你足够的绳索来搬起石头砸自己的脚。
在前面的示例中,我们看到了如何使用用户定义的类型来表达在内置类型上完成的所有操作。C++ 的另一个目标是以通用的方式编写代码,在这种方式中,我们可以替换一个用户定义的类,该类模仿内置类型之一的语义,如float
、double
、int
等:
//------------- from SmartValue.cpp
template <class T>
T Accumulate( T a[] , int count ) {
T value = 0;
for( int i=0; i<count; ++ i) { value += a[i]; }
return value;
}
int main(){
//----- Templated version of SmartFloat
SmartValue<double> y[] = { 10,20.0,30,40 };
double res = Accumulate(y,4);
cout << res << endl;
}
The C++ programming language supports different programming paradigms and the three principles outlined previously are just some of them. The language gives support for constructs that can help create robust types (domain-specific) for writing better code. These three principles gave us a powerful and fast programming language for sure. Modern C++ did add a lot of new abstractions to make the life of a programmer easier. But the three design principles outlined previously have not been sacrificed in any way to achieve those objectives. This was partly possible because of the meta programming support the language had due to the inadvertent Turing completeness of the template mechanism. Read about template meta programming (TMP) and Turing Completeness with the help of your favorite search engine.
在过去的十年里,编程语言世界发生了很大的变化,这些变化应该反映在 C++ 编程语言的新形象中。现代 C++ 的大部分创新涉及处理高级抽象和引入函数式编程结构来支持语言级并发。大多数现代语言都有一个垃圾收集器,运行时管理这些复杂性。C++ 编程语言没有自动垃圾收集作为语言标准的一部分。C++ 编程语言以其对零成本抽象(您不为您不使用的东西付费)和最大运行时性能的隐式保证,不得不求助于许多编译时技巧和元编程技术来实现 C#、Java 或 Scala 等语言支持的抽象级别。其中一些将在下面的章节中概述,您可以自己深入研究这些主题。网站http://en.cppreference.com是一个提高你的 C++ 编程语言知识的好网站。
现代 C++ 语言编译器在从程序员指定的表达式和语句中推导类型方面做得很好。大多数现代编程语言都支持类型推断,现代 C++ 也是如此。这是从函数式编程语言如 Haskell 和 ML 中借用的一个习惯用法。C#和 Scala 编程语言已经提供了类型推断。我们将编写一个小程序来启动类型推断:
//----- AutoFirst.cpp
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<string> vt = {"first", "second", "third", "fourth"};
//--- Explicitly specify the Type ( makes it verbose)
for (vector<string>::iterator it = vt.begin();
it != vt.end(); ++ it)
cout << *it << " ";
//--- Let the compiler infer the type for us
for (auto it2 = vt.begin(); it2 != vt.end(); ++ it2)
cout << *it2 << " ";
return 0;
}
auto
关键字指定变量的类型将由编译器根据表达式中指定的函数的初始化和返回值来推导。在这个特殊的例子中,我们没有得到多少。随着我们的声明变得越来越复杂,最好让编译器进行类型推断。我们的代码清单将使用 auto 来简化整本书的代码。现在,让我们写一个简单的程序,让这个想法更加清晰:
//----- AutoSecond.cpp
#include <iostream>
#include <vector>
#include <initializer_list>
using namespace std;
int main() {
vector<double> vtdbl = {0, 3.14, 2.718, 10.00};
auto vt_dbl2 = vtdbl; // type will be deduced
auto size = vt_dbl2.size(); // size_t
auto &rvec = vtdbl; // specify a auto reference
cout << size << endl;
// Iterate - Compiler infers the type
for ( auto it = vtdbl.begin(); it != vtdbl.end(); ++ it)
cout << *it << " ";
// 'it2' evaluates to iterator to vector of double
for (auto it2 = vt_dbl2.begin(); it2 != vt_dbl2.end(); ++ it2)
cout << *it2 << " ";
// This will change the first element of vtdbl vector
rvec[0] = 100;
// Now Iterate to reflect the type
for ( auto it3 = vtdbl.begin(); it3 != vtdbl.end(); ++ it3)
cout << *it3 << " ";
return 0;
}
前面的代码演示了在编写现代 C++ 代码时类型推理的使用。C++ 编程语言还有一个新的关键字,可以帮助查询作为参数给出的表达式类型。关键词的一般形式是decltype(<expr>)
。以下程序有助于演示这个特定关键字的用法:
//---- Decltype.cpp
#include <iostream>
using namespace std;
int foo() { return 10; }
char bar() { return 'g'; }
auto fancy() -> decltype(1.0f) { return 1;} //return type is float
int main() {
// Data type of x is same as return type of foo()
// and type of y is same as return type of bar()
decltype(foo()) x;
decltype(bar()) y;
//--- in g++, Should print i => int
cout << typeid(x).name() << endl;
//--- in g++, Should print c => char
cout << typeid(y).name() << endl;
struct A { double x; };
const A* a = new A();
decltype(a->x) z; // type is double
decltype((a->x)) t= z; // type is const double&
//--- in g++, Should print d => double
cout << typeid(z).name() << endl;
cout << typeid(t).name() << endl;
//--- in g++, Should print f => float
cout << typeid(decltype(fancy())).name() << endl;
return 0;
}
decltype
是一个编译时构造,它有助于指定变量的类型(编译器将努力找出它),也有助于我们强制变量的类型(参见前面的fancy()
函数)。
经典的 C++ 有某种特殊的变量初始化语法。现代 C++ 支持统一初始化(我们已经在类型推断部分看到了例子)。该语言为开发人员提供帮助类,以支持自定义类型的统一初始化:
//----------------Initialization.cpp
#include <iostream>
#include <vector>
#include <initializer_list>
using namespace std;
template <class T>
struct Vector_Wrapper {
std::vector<T> vctr;
Vector_Wrapper(std::initializer_list<T> l) : vctr(l) {}
void Append(std::initializer_list<T> l)
{ vctr.insert(vctr.end(), l.begin(), l.end());}
};
int main() {
Vector_Wrapper<int> vcw = {1, 2, 3, 4, 5}; // list-initialization
vcw.Append({6, 7, 8}); // list-initialization in function call
for (auto n : vcw.vctr) { std::cout << n << ' '; }
std::cout << '\n';
}
前面的清单显示了如何为程序员创建的自定义类启用初始化列表。
在 C++ 11 和更高版本中,作为标准语言的一部分,支持变量模板。变量模板是一个模板类或模板函数,它在模板参数中取一个变量数。在经典的 C++ 中,模板实例化是用固定数量的参数进行的。类级和函数级都支持变量模板。在本节中,我们将讨论变量函数,因为它们广泛用于编写函数式程序、编译时编程(元编程)和可管道函数:
//Variadic.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
using namespace std;
//--- add given below is a base case for ending compile time
//--- recursion
int add() { return 0; } // end condition
//---- Declare a Variadic function Template
//---- ... is called parameter pack. The compiler
//--- synthesize a function based on the number of arguments
//------ given by the programmer.
//----- decltype(auto) => Compiler will do Type Inference
template<class T0, class ... Ts>
decltype(auto) add(T0 first, Ts ... rest) {
return first + add(rest ...);
}
int main() { int n = add(0,2,3,4); cout << n << endl; }
在前面的代码中,编译器根据传递的参数数量合成一个函数。编译器理解add
是一个变量函数,在编译时通过递归解包参数来生成代码。当编译器处理完所有参数后,编译时递归将停止。基本用例版本是对编译器停止递归的提示。下一个程序展示了如何使用可变模板和完美转发来编写一个接受任意数量参数的函数:
//Variadic2.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
using namespace std;
//--------- Print values to the console for basic types
//-------- These are base case versions
void EmitConsole(int value) { cout << "Integer: " << value << endl; }
void EmitConsole(double value) { cout << "Double: " << value << endl; }
void EmitConsole(const string& value){cout << "String: "<<value<< endl; }
EmitConsole
的三个变体将参数打印到控制台。我们有打印int
、double
和string
的功能。使用这些函数作为基本案例,我们将编写一个使用通用引用和完美转发的函数来编写采用任意值的函数:
template<typename T>
void EmitValues(T&& arg) { EmitConsole(std::forward<T>(arg)); }
template<typename T1, typename... Tn>
void EmitValues(T1&& arg1, Tn&&... args){
EmitConsole(std::forward<T1>(arg1));
EmitValues(std::forward<Tn>(args)...);
}
int main() { EmitValues(0,2.0,"Hello World",4); }
如果你已经用 C++ 编程很长时间了,你可能会熟悉这样一个事实:C++ 引用可以帮助你别名化一个变量,你可以对引用赋值来反映别名化变量的变化。C++ 支持的引用类型被称为左值引用(因为它们是对赋值左侧变量的引用)。以下代码片段显示了左值引用的使用:
//---- Lvalue.cpp
#include <iostream>
using namespace std;
int main() {
int i=0;
cout << i << endl; //prints 0
int& ri = i;
ri = 20;
cout << i << endl; // prints 20
}
int&
是左值引用的一个实例。在现代 C++ 中,有右值引用的概念。右值被定义为任何不是左值的东西,这种东西可以出现在赋值的右边。在经典的 C++ 中,没有右值引用的概念。现代 C++ 引入了它:
///---- Rvaluref.cpp
#include <iostream>using namespace std;
int main() {
int&& j = 42;int x = 3,y=5; int&& z = x + y; cout << z << endl;
z = 10; cout << z << endl;j=20;cout << j << endl;
}
右值引用由两个&&
表示。以下程序将清楚地演示调用函数时右值引用的使用:
//------- RvaluerefCall.cpp
#include <iostream>
using namespace std;
void TestFunction( int & a ) {cout << a << endl;}
void TestFunction( int && a ){
cout << "rvalue references" << endl;
cout << a << endl;
}
int main() {
int&& j = 42;
int x = 3,y=5;
int&& z = x + y;
TestFunction(x + y ); // Should call rvalue reference function
TestFunction(j); // Calls Lvalue Refreence function
}
右值引用的真正威力在内存管理中显而易见。C++ 编程语言有复制构造函数和赋值操作符的概念。它们主要复制源对象内容。在右值引用的帮助下,可以通过交换指针来避免昂贵的复制,因为右值引用是临时的或中间表达式。下一节将对此进行演示。
C++ 编程语言为我们设计的每个类隐式保证了一个复制构造函数、赋值操作符和一个析构函数(有时是虚拟的)。这意味着在克隆对象或分配给现有对象时进行资源管理。有时复制一个对象非常昂贵,所有权的移动(通过指针)有助于编写快速代码。现代 C++ 拥有提供移动构造函数和移动赋值操作符的功能,以帮助开发人员在创建新对象或向新对象赋值的过程中避免复制大对象。右值引用可以作为编译器的提示,当涉及临时对象时,构造函数的移动版本或赋值的移动版本更适合上下文:
//----- FloatBuffer.cpp
#include <iostream>
#include <vector>
using namespace std;
class FloatBuffer {
double *bfr; int count;
public:
FloatBuffer():bfr(nullptr),count(0){}
FloatBuffer(int pcount):bfr(new double[pcount]),count(pcount){}
// Copy constructor.
FloatBuffer(const FloatBuffer& other) : count(other.count)
, bfr(new double[other.count])
{ std::copy(other.bfr, other.bfr + count, bfr); }
// Copy assignment operator - source code is obvious
FloatBuffer& operator=(const FloatBuffer& other) {
if (this != &other) {
if ( bfr != nullptr)
delete[] bfr; // free memory of the current object
count = other.count;
bfr = new double[count]; //re-allocate
std::copy(other.bfr, other.bfr + count, bfr);
}
return *this;
}
// Move constructor to enable move semantics
// The Modern STL containers supports move sementcis
FloatBuffer(FloatBuffer&& other) : bfr(nullptr) , count(0) {
cout << "in move constructor" << endl;
// since it is a move constructor, we are not copying elements from
// the source object. We just assign the pointers to steal memory
bfr = other.bfr;
count = other.count;
// Now that we have grabbed our memory, we just assign null to
// source pointer
other.bfr = nullptr;
other.count = 0;
}
// Move assignment operator.
FloatBuffer& operator=(FloatBuffer&& other) {
if (this != &other)
{
// Free the existing resource.
delete[] bfr;
// Copy the data pointer and its length from the
// source object.
bfr = other.bfr;
count = other.count;
// We have stolen the memory, now set the pinter to null
other.bfr = nullptr;
other.count = 0;
}
return *this;
}
};
int main() {
// Create a vector object and add a few elements to it.
// Since STL supports move semantics move methods will be called.
// in this particular case (Modern Compilers are smart)
vector<FloatBuffer> v;
v.push_back(FloatBuffer(25));
v.push_back(FloatBuffer(75));
}
std::move
函数可用于指示(在传递参数时)候选对象是可移动的,编译器将调用适当的方法(移动赋值或移动构造函数)来优化与内存管理相关的成本。基本上,std::move
是一个参考值的static_cast
。
对于 C++ 编程语言来说,管理对象生存期一直是一个有问题的领域。如果开发人员不小心,程序可能会泄漏内存并降低性能。智能指针是原始指针周围的包装类,其中诸如取消引用(*)和引用(->)等操作符被重载。智能指针可以进行对象生存期管理,充当有限形式的垃圾收集,释放内存等等。现代 C++ 语言有:
unique_ptr<T>
shared_ptr<T>
weak_ptr<T>
一个unique_ptr<T>
是一个原始指针的包装器,它拥有包装器的独占所有权。以下代码片段将演示<unique_ptr>
的使用:
//---- Unique_Ptr.cpp
#include <iostream>
#include <deque>#include <memory>
using namespace std;
int main( int argc , char **argv ) {
// Define a Smart Pointer for STL deque container...
unique_ptr< deque<int> > dq(new deque<int>() );
//------ populate values , leverages -> operator
dq->push_front(10); dq->push_front(20);
dq->push_back(23); dq->push_front(16);
dq->push_back(41);
auto dqiter = dq->begin();
while ( dqiter != dq->end())
{ cout << *dqiter << "\n"; dqiter++ ; }
//------ SmartPointer will free reference
//------ and it's dtor will be called here
return 0;
}
std::shared_ptr
是一个智能指针,它使用引用计数来跟踪对对象特定实例的引用。当指向底层对象的最后一个剩余shared_ptr
被破坏或重置时,底层对象被破坏:
//----- Shared_Ptr.cpp
#include <iostream>
#include <memory>
#include <stdio.h>
using namespace std;
////////////////////////////////////////
// Even If you pass shared_ptr<T> instance
// by value, the update is visible to callee
// as shared_ptr<T>'s copy constructor reference
// counts to the orgininal instance
//
void foo_byvalue(std::shared_ptr<int> i) { (*i)++ ;}
///////////////////////////////////////
// passed by reference,we have not
// created a copy.
//
void foo_byreference(std::shared_ptr<int>& i) { (*i)++ ; }
int main(int argc, char **argv )
{
auto sp = std::make_shared<int>(10);
foo_byvalue(sp);
foo_byreference(sp);
//--------- The output should be 12
std::cout << *sp << std::endl;
}
std:weak_ptr
是原始指针的容器。它是作为shared_ptr
的副本创建的。weak_ptr
副本的存在或销毁对shared_ptr
或其其他副本没有影响。在一个shared_ptr
的所有副本被销毁后,所有weak_ptr
副本都变成空的。下面的程序演示了一种机制,帮助我们使用weak_ptr
检测失效的指针:
//------- Weak_Ptr.cpp
#include <iostream>
#include <deque>
#include <memory>
using namespace std;
int main( int argc , char **argv )
{
std::shared_ptr<int> ptr_1(new int(500));
std::weak_ptr<int> wptr_1 = ptr_1;
{
std::shared_ptr<int> ptr_2 = wptr_1.lock();
if(ptr_2)
{
cout << *ptr_2 << endl; // this will be exeucted
}
//---- ptr_2 will go out of the scope
}
ptr_1.reset(); //Memory is deleted.
std::shared_ptr<int> ptr_3= wptr_1.lock();
//-------- Always else part will be executed
//-------- as ptr_3 is nullptr now
if(ptr_3)
cout << *ptr_3 << endl;
else
cout << "Defunct Pointer" << endl;
return 0;
}
经典 C++ 有一个智能指针类型叫做auto_ptr
,它已经从语言标准中删除了。需要使用unique_ptr
来代替。
C++ 语言的主要新增内容之一是 Lambda 函数和 Lambda 表达式。它们是匿名函数,程序员可以在调用点定义它们来执行一些逻辑。这简化了逻辑,代码的可读性也显著提高。
与其定义什么是 Lambda 函数,不如让我们写一段代码,帮助我们计算vector<int>
中正数的个数。在这种情况下,我们需要过滤掉负值并计算其余的。我们将使用一个 STL count_if
来编写代码:
//LambdaFirst.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
auto num_vect =
vector<int>{ 10, 23, -33, 15, -7, 60, 80};
//---- Define a Lambda Function to Filter out negatives
auto filter = [](int const value) {return value > 0; };
auto cnt= count_if(
begin(num_vect), end(num_vect),filter);
cout << cnt << endl;
}
在前面的代码片段中,变量过滤器被分配了一个匿名函数,我们在count_if STL
函数中使用过滤器。现在,让我们编写一个简单的 Lambda 函数,我们将在函数调用站点指定它。我们将使用 STL 累加来聚合向量中的值:
//-------------- LambdaSecond.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
int main() {
auto num_vect =
vector<int>{ 10, 23, -33, 15, -7, 60, 80};
//-- Define a BinaryOperation Lambda at the call site
auto accum = std::accumulate(
std::begin(num_vect), std::end(num_vect), 0,
[](auto const s, auto const n) {return s + n;});
cout << accum << endl;
}
在经典的 C++ 中,当使用 STL 时,我们通过重载函数运算符来编写转换过滤器和在 STL 容器上执行约简,从而广泛使用函数对象或函子:
//----- LambdaThird.cpp
#include <iostream>
#include <numeric>
using namespace std;
//////////////////////////
// Functors to add and multiply two numbers
template <typename T>
struct addition{
T operator () (const T& init, const T& a ) { return init + a; }
};
template <typename T>
struct multiply {
T operator () (const T& init, const T& a ) { return init * a; }
};
int main()
{
double v1[3] = {1.0, 2.0, 4.0}, sum;
sum = accumulate(v1, v1 + 3, 0.0, addition<double>());
cout << "sum = " << sum << endl;
sum = accumulate(v1,v1+3,0.0, [] (const double& a ,const double& b ) {
return a +b;
});
cout << "sum = " << sum << endl;
double mul_pi = accumulate(v1, v1 + 3, 1.0, multiply<double>());
cout << "mul_pi = " << mul_pi << endl;
mul_pi= accumulate(v1,v1+3,1, [] (const double& a , const double& b ){
return a *b;
});
cout << "mul_pi = " << mul_pi << endl;
}
下面的程序通过编写一个玩具分类程序清楚地演示了 Lambda 的用法。我们将展示如何使用函数对象和 Lambdas 来编写等效的代码。代码是以通用的方式编写的,但它假设数字是预期的(double
、float
、integer
,或用户定义的等价物):
/////////////////
//-------- LambdaFourth.cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//--- Generic functions for Comparison and Swap
template <typename T>
bool Cmp( T& a , T&b ) {return ( a > b ) ? true: false;}
template <typename T>
void Swap( T& a , T&b ) { T c = a;a = b;b = c;}
Cmp
和Swap
是通用函数,分别用于在执行排序操作时比较相邻元素和交换元素:
template <typename T>
void BubbleSortFunctor( T *arr , int length ) {
for( int i=0; i< length-1; ++ i )
for(int j=i+1; j< length; ++ j )
if ( Cmp( arr[i] , arr[j] ) )
Swap(arr[i],arr[j] );
}
有了 Cmp 和 Swap,编写一个冒泡排序是一件简单的事情。我们需要一个嵌套循环,在这里我们将比较两个元素,如果 Cmp 返回 true,我们将调用 Swap 来交换值:
template <typename T>
void BubbleSortLambda( T *arr , int length ) {
auto CmpLambda = [] (const auto& a , const auto& b )
{ return ( a > b ) ? true: false; };
auto SwapLambda = [] ( auto& a , auto& b )
{ auto c = a;a = b;b = c;};
for( int i=0; i< length-1; ++ i )
for(int j=i+1; j< length; ++ j )
if ( CmpLambda( arr[i] , arr[j] ) )
SwapLambda (arr[i],arr[j] );
}
在前面的例程中,我们将比较和交换函数定义为 Lambdas。Lambda 函数是一种内联指定一段代码或表达式的机制,通常称为匿名函数。该定义可以用 C++ 语言指定的语法给出,并且可以分配给变量、作为参数传递或从函数返回。在前面的函数中,变量CmpLambda
和SwapLambda
是 Lambda 语法中指定的匿名函数的例子。Lambda 函数的主体与前面的函数版本没有太大区别。要了解关于 Lambda 函数和表达式的更多信息,您可以参考位于http://en.cppreference.com/w/cpp/language/lambda的页面。
template <typename T>
void Print( const T& container){
for(auto i = container.begin() ; i != container.end(); ++ i )
cout << *i << "\n" ;
}
Print
例程只是循环遍历容器中的元素,并将内容打印到控制台:
int main( int argc , char **argv ){
double ar[4] = {20,10,15,-41};
BubbleSortFunctor(ar,4);
vector<double> a(ar,ar+4);
Print(a);
cout << "=========================================" << endl;
ar[0] = 20;ar[1] = 10;ar[2] = 15;ar[3] = -41;
BubbleSortLambda(ar,4);
vector<double> a1(ar,ar+4);
Print(a1);
cout << "=========================================" << endl;
}
Lambdas 的一个优点是,您可以将两个函数组合在一起,创建一个函数组合,就像您在数学中所做的那样(使用最喜欢的搜索引擎阅读数学和函数编程上下文中的函数组合)。下面的程序演示了这个想法。这是一个玩具实现,编写通用实现超出了本章的范围:
//------------ Compose.cpp
//----- g++ -std=c++ 1z Compose.cpp
#include <iostream>
using namespace std;
//---------- base case compile time recursion
//---------- stops here
template <typename F, typename G>
auto Compose(F&& f, G&& g)
{ return [=](auto x) { return f(g(x)); };}
//----- Performs compile time recursion based
//----- on number of parameters
template <typename F, typename... R>
auto Compose(F&& f, R&&... r){
return [=](auto x) { return f(Compose(r...)(x)); };
}
Compose
是一个变量模板函数,编译器通过递归扩展Compose
参数生成代码,直到处理完所有参数。在前面的代码中,我们使用了[=]
来指示编译器,我们应该通过值来捕获 Lambda 主体中引用的所有变量。您可以在函数式编程的上下文中学习更多关于闭包和变量捕获的知识。C++ 语言通过值(以及使用[&]
)或通过明确指定要捕获的变量(如[&var]
)为Capture
变量提供了灵活性。
函数式编程范式基于美国数学家阿隆佐·邱奇发明的一种叫做 Lambda 演算的数学形式主义。Lambda 演算只支持一元函数,currying 是一种技术,它将一个多参数函数分解成一系列一次接受一个参数的函数求值。
使用 Lambdas 并以特定的方式编写函数,我们可以在 C++ 中模拟 currying:
auto CurriedAdd3(int x) {
return [x](int y) { //capture x
return [x, y](int z){ return x + y + z; };
};
};
部分函数应用涉及将具有多个参数的函数转换为固定数量的参数。如果固定的参数数量小于函数的 arity(参数计数),将返回一个新的函数,该函数需要其余的参数。当接收到所有参数时,将调用该函数。我们可以将部分应用视为某种形式的记忆,在那里缓存参数,直到我们接收到所有参数并调用它们。
在下面的代码片段中,我们使用了像模板参数包和变量模板这样的构造。模板参数包是接受零个或多个模板参数(非类型、类型或模板)的模板参数。函数参数包是接受零个或多个函数参数的函数参数。至少包含一个参数包的模板称为变量模板。了解sizeof...
构造需要一个关于参数包和变量模板的好主意。
template <typename... Ts>
auto PartialFunctionAdd3(Ts... xs) {
//---- http://en.cppreference.com/w/cpp/language/parameter_pack
//---- http://en.cppreference.com/w/cpp/language/sizeof...
static_assert(sizeof...(xs) <= 3);
if constexpr (sizeof...(xs) == 3){
// Base case: evaluate and return the sum.
return (0 + ... + xs);
}
else{
// Recursive case: bind `xs...` and return another
return [xs...](auto... ys){
return PartialFunctionAdd3(xs..., ys...);
};
}
}
int main() {
// ------------- Compose two functions together
//----https://en.wikipedia.org/wiki/Function_composition
auto val = Compose(
[](int const a) {return std::to_string(a); },
[](int const a) {return a * a; })(4); // val = "16"
cout << val << std::endl; //should print 16
// ----------------- Invoke the Curried function
auto p = CurriedAdd3(4)(5)(6);
cout << p << endl;
//-------------- Compose a set of function together
auto func = Compose(
[](int const n) {return std::to_string(n); },
[](int const n) {return n * n; },
[](int const n) {return n + n; },
[](int const n) {return std::abs(n); });
cout << func(5) << endl;
//----------- Invoke Partial Functions giving different arguments
PartialFunctionAdd3(1, 2, 3);
PartialFunctionAdd3(1, 2)(3);
PartialFunctionAdd3(1)(2)(3);
}
函数包装器是可以将任何函数、函数对象或 Lambdas 包装成可复制对象的类。包装器的类型取决于类的函数原型。<functional>
头中的std::function(<prototype>)
代表一个函数包装:
//---------------- FuncWrapper.cpp Requires C++ 17 (-std=c++ 1z )
#include <functional>
#include <iostream>
using namespace std;
//-------------- Simple Function call
void PrintNumber(int val){ cout << val << endl; }
// ------------------ A class which overloads function operator
struct PrintNumber {
void operator()(int i) const { std::cout << i << '\n';}
};
//------------ To demonstrate the usage of method call
struct FooClass {
int number;
FooClass(int pnum) : number(pnum){}
void PrintNumber(int val) const { std::cout << number + val<< endl; }
};
int main() {
// ----------------- Ordinary Function Wrapped
std::function<void(int)>
displaynum = PrintNumber;
displaynum(0xF000);
std::invoke(displaynum,0xFF00); //call through std::invoke
//-------------- Lambda Functions Wrapped
std::function<void()> lambdaprint = []() { PrintNumber(786); };
lambdaprint();
std::invoke(lambdaprint);
// Wrapping member functions of a class
std::function<void(const FooClass&, int)>
class display = &FooClass::PrintNumber;
// creating an instance
const FooClass fooinstance(100);
class display (fooinstance,100);
}
在接下来的部分中,我们将在代码中广泛使用std::function
,因为它有助于将函数调用作为数据进行拖动。
Unix 操作系统的命令行外壳允许一个函数的标准输出通过管道传输到另一个函数,以形成一个过滤器链。后来,这个特性成为大多数操作系统提供的命令行外壳的一部分。在编写函数式代码时,当我们通过函数组合来组合方法时,代码会因为深度嵌套而变得难以阅读。现在,使用现代 C++ 我们可以重载 pipe ( |
)操作符,允许将几个函数链接在一起,就像我们在 Unix shell 或 Windows PowerShell 控制台中执行命令一样。这就是为什么有人把 LISP 语言重新命名为“许多恼人的和愚蠢的括号”。RxCpp 库广泛使用|
运算符来组合函数。下面的代码帮助我们理解如何创建可管道化的函数。我们将看看如何在原则上实现这一点。这里给出的代码仅用于说明目的:
//---- PipeFunc2.cpp
//-------- g++ -std=c++ 1z PipeFunc2.cpp
#include <iostream>
using namespace std;
struct AddOne {
template<class T>
auto operator()(T x) const { return x + 1; }
};
struct SumFunction {
template<class T>
auto operator()(T x,T y) const { return x + y;} // Binary Operator
};
前面的代码创建了一组可调用类,它将被用作组成函数链的一部分。现在,我们需要创建一个机制来将任意函数转换为闭包:
//-------------- Create a Pipable Closure Function (Unary)
//-------------- Uses Variadic Templates Paramter pack
template<class F>
struct PipableClosure : F{
template<class... Xs>
PipableClosure(Xs&&... xs) : // Xs is a universal reference
F(std::forward<Xs>(xs)...) // perfect forwarding
{}
};
//---------- A helper function which converts a Function to a Closure
template<class F>
auto MakePipeClosure(F f)
{ return PipableClosure<F>(std::move(f)); }
// ------------ Declare a Closure for Binary
//------------- Functions
//
template<class F>
struct PipableClosureBinary {
template<class... Ts>
auto operator()(Ts... xs) const {
return MakePipeClosure([=](auto x) -> decltype(auto)
{ return F()(x, xs...);}); }
};
//------- Declare a pipe operator
//------- uses perfect forwarding to invoke the function
template<class T, class F> //---- Declare a pipe operator
decltype(auto) operator|(T&& x, const PipableClosure<F>& pfn)
{ return pfn(std::forward<T>(x)); }
int main() {
//-------- Declare a Unary Function Closure
const PipableClosure<AddOne> fnclosure = {};
int value = 1 | fnclosure| fnclosure;
std::cout << value << std::endl;
//--------- Decalre a Binary function closure
const PipableClosureBinary<SumFunction> sumfunction = {};
int value1 = 1 | sumfunction(2) | sumfunction(5) | fnclosure;
std::cout << value1 << std::endl;
}
现在,我们可以创建一个以一元函数为参数的PipableClosure
实例,并将对闭包的一系列调用链接(或组合)在一起。前面的代码片段应该在控制台上打印三个。我们还创建了一个PipableBinaryClosure
实例,将一元函数和二元函数串在一起。
到目前为止,我们已经介绍了从 C++ 11 标准开始的语言最重要的语义变化。本章的目的是强调在编写惯用的现代 C++ 程序时可能有用的关键变化。C++ 17 标准在语言中加入了更多的东西。我们将突出这种语言的几个特点来结束这次讨论。
C++ 17 标准增加了对 fold 表达式的支持,以简化变量函数的生成。编译器进行模式匹配,并通过推断程序员的意图来生成代码。下面的代码片段演示了这个想法:
//---------------- Folds.cpp
//--------------- Requires C++ 17 (-std=c++ 1z )
//--------------- http://en.cppreference.com/w/cpp/language/fold
#include <functional>
#include <iostream>
using namespace std;
template <typename... Ts>
auto AddFoldLeftUn(Ts... args) { return (... + args); }
template <typename... Ts>
auto AddFoldLeftBin(int n,Ts... args){ return (n + ... + args);}
template <typename... Ts>
auto AddFoldRightUn(Ts... args) { return (args + ...); }
template <typename... Ts>
auto AddFoldRightBin(int n,Ts... args) { return (args + ... + n); }
template <typename T,typename... Ts>
auto AddFoldRightBinPoly(T n,Ts... args) { return (args + ... + n); }
template <typename T,typename... Ts>
auto AddFoldLeftBinPoly(T n,Ts... args) { return (n + ... + args); }
int main() {
auto a = AddFoldLeftUn(1,2,3,4);
cout << a << endl;
cout << AddFoldRightBin(a,4,5,6) << endl;
//---------- Folds from Right
//---------- should produce "Hello World C++"
auto b = AddFoldRightBinPoly("C++ "s,"Hello "s,"World "s );
cout << b << endl;
//---------- Folds (Reduce) from Left
//---------- should produce "Hello World C++"
auto c = AddFoldLeftBinPoly("Hello "s,"World "s,"C++ "s );
cout << c << endl;
}
控制台上的预期输出如下
10
25
Hello World C++
Hello World C++
变体的古怪定义是“类型安全联合”。我们可以在定义变体时给出一个类型列表作为模板参数。在任何给定时间,对象将只保存模板参数列表中的一种类型的数据。如果我们试图访问不保存当前值的索引,将会抛出std::bad_variant_access
。以下代码不处理此异常:
//------------ Variant.cpp
//------------- g++ -std=c++ 1z Variant.cpp
#include <variant>
#include <string>
#include <cassert>
#include <iostream>
using namespace std;
int main(){
std::variant<int, float,string> v, w;
v = 12.0f; // v contains now contains float
cout << std::get<1>(v) << endl;
w = 20; // assign to int
cout << std::get<0>(w) << endl;
w = "hello"s; //assign to string
cout << std::get<2>(w) << endl;
}
现代 C++ 支持语言级并发、内存保证和异步执行等特性,这些将在接下来的两章中介绍。该语言支持可选数据类型和std::any
类型。最重要的特性之一是大多数 STL 算法的并行版本。
在本节中,我们将在自己编写的自定义类型上实现基于范围的 for 循环,以帮助您理解如何将本章前面提到的所有内容组合起来编写支持现代习惯用法的程序。我们将实现一个类,该类在一个界限内返回一系列数字,并将实现对基于范围的 for 循环的值迭代的基础结构支持。首先,我们通过利用基于范围的 for 循环来编写“Iterable/Iterator”(又名“Enumerable/Enumerable”)版本。经过一些调整后,实现将被转换为可观察/观察者(反应式编程的关键接口)模式:这里的可观察/观察者模式的实现只是为了说明的目的,不应该被认为是这些模式的工业实力实现。
下面的iterable
类是嵌套类:
// Iterobservable.cpp
// we can use Range Based For loop as given below (see the main below)
// for (auto l : EnumerableRange<5, 25>()) { std::cout << l << ' '; }
// std::cout << endl;
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
#include <functional>
using namespace std;
template<long START, long END>
class EnumerableRange {
public:
class iterable : public std::iterator<
std::input_iterator_tag, // category
long, // value_type
long, // difference_type
const long*, // pointer type
long> // reference type
{
long current_num = START;
public:
reference operator*() const { return current_num; }
explicit iterable(long val = 0) : current_num(val) {}
iterable& operator++() {
current_num = ( END >= START) ? current_num + 1 :
current_num - 1;
return *this;
}
iterable operator++(int) {
iterable retval = *this; ++(*this); return retval;
}
bool operator==(iterable other) const
{ return current_num == other.current_num; }
bool operator!=(iterable other) const
{ return !(*this == other); }
};
前面的代码实现了一个从std::iterator
派生的内部类,以满足通过基于范围的循环可枚举的类型的要求。我们现在将编写两个公共方法(begin()
和end()
,因此类的消费者可以使用基于范围的循环:
iterable begin() { return iterable(START); }
iterable end() { return iterable(END >= START ? END + 1 :
END - 1); }
};
现在,我们可以编写代码来使用前面的类,如下所示:
for (long l : EnumerableRange<5, 25>())
{ std::cout << l << ' '; }
上一章我们定义了IEnumerable<T>
界面。这个想法是坚持使用反应式扩展的文档。iterable 类与前一章中的IEnumerable<T>
实现非常相似。如前一章所述,如果我们稍微调整一下代码,前面的类可以基于 push。让我们编写一个包含三种方法的OBSERVER
类。我们将使用标准库中可用的函数包装来定义方法:
struct OBSERVER {
std::function<void(const long&)> ondata;
std::function<void()> oncompleted;
std::function<void(const std::exception &)> onexception;
};
这里给出的ObservableRange
类包含一个vector<T>
,用于存储订户列表。当生成新号码时,该事件将通知所有订户。如果我们从异步方法中调度通知调用,那么消费者就与范围流的生产者分离了。我们还没有为以下类实现IObserver/IObserver<T>
接口,但是我们可以通过订阅方法订阅通知:
template<long START, long END>
class ObservableRange {
private:
//---------- Container to store observers
std::vector<
std::pair<const OBSERVER&,int>> _observers;
int _id = 0;
我们将把用户列表存储在std::vector
中作为std::pair
。std::pair
中的第一个值是对OBSERVER
的引用,std::pair
中的第二个值是唯一标识订户的整数。消费者应该使用 subscribe 方法返回的 ID 取消订阅:
//---- The following implementation of iterable does
//---- not allow to take address of the pointed value [ &(*it)
//---- Eg- &(*iterable.begin()) will be ill-formed
//---- Code is just for demonstrate Obervable/Observer
class iterable : public std::iterator<
std::input_iterator_tag, // category
long, // value_type
long, // difference_type
const long*, // pointer type
long> // reference type
{
long current_num = START;
public:
reference operator*() const { return current_num; }
explicit iterable(long val = 0) : current_num(val) {}
iterable& operator++() {
current_num = ( END >= START) ? current_num + 1 :
current_num - 1;
return *this;
}
iterable operator++(int) {
iterable retval = *this; ++(*this); return retval;
}
bool operator==(iterable other) const
{ return current_num == other.current_num; }
bool operator!=(iterable other) const
{ return !(*this == other); }
};
iterable begin() { return iterable(START); }
iterable end() { return iterable(END >= START ? END + 1 : END - 1); }
// generate values between the range
// This is a private method and will be invoked from the generate
// ideally speaking, we should invoke this method with std::asnyc
void generate_async()
{
auto& subscribers = _observers;
for( auto l : *this )
for (const auto& obs : subscribers) {
const OBSERVER& ob = obs.first;
ob.ondata(l);
}
}
//----- The public interface of the call include generate which triggers
//----- the generation of the sequence, subscribe/unsubscribe pair
public:
//-------- the public interface to trigger generation
//-------- of thevalues. The generate_async can be executed
//--------- via std::async to return to the caller
void generate() { generate_async(); }
//---------- subscribe method. The clients which
//----------- expects notification can register here
int subscribe(const OBSERVER& call) {
// https://en.cppreference.com/w/cpp/container/vector/emplace_back
_observers.emplace_back(call, ++ _id);
return _id;
}
//------------ has just stubbed unsubscribe to keep
//------------- the listing small
void unsubscribe(const int subscription) {}
};
int main() {
//------ Call the Range based enumerable
for (long l : EnumerableRange<5, 25>())
{ std::cout << l << ' '; }
std::cout << endl;
// instantiate an instance of ObservableRange
auto j = ObservableRange<10,20>();
OBSERVER test_handler;
test_handler.ondata = [=](const long & r)
{cout << r << endl; };
//---- subscribe to the notifiactions
int cnt = j.subscribe(test_handler);
j.generate(); //trigget events to generate notifications
return 0;
}
在本章中,我们学习了 C++ 程序员在编写 Reactive 程序时应该熟悉的编程语言特性,或者任何种类的程序。我们讨论了类型推断、变量模板、右值引用和移动语义、Lambda 函数、初等函数编程、可管道操作符以及迭代器和观察器的实现。在下一章中,我们将学习 C++ 编程语言提供的并发编程支持。