10.3 定制操作
我们在使用泛型算法时,泛型算法会调用序列中元素的运算符,但是有时这些类型并不支持某些运算符,或者说我们没有为这个类型,定义该运算符。
这时我们可以通过定制操作,来完成代替这些运算符完成某些操作。
比如sort()支持传入一个谓词,这个谓词用来代替<运算符来比较元素大小。
谓词根据需要的参数数量,分为一元谓词和二元谓词。
谓词需要满足的条件,将在以后有介绍,现在就我的理解来说,谓词需要为任意元素对比提供一个一致的标准。
排序算法分为稳定和不稳定的版本,稳定的版本的sort,相同元素的相对位置不会发生改变,但是不稳定的sort,相同元素的相对位置可能发生改变。
一般情况下,相对位置改变没有关系。但是有时我们对相等的定义不一样,比如长度相等就是相等,这时候可能需要考虑相对位置的变换。
stable_sort();
练习
10.11
在我写的这个例子中,sort和stable_sort排序之后的元素都是一样的,但是学过排序算法的话的人一般都会知道排序算法分为稳定和不稳定两种。
bool isShorter(const string& str1,const string& str2) {
return str1.size() < str2.size();
}
vector<string> vec = {"a","c","be23","e123","1ca","x","aaa","z123","q","casd","asdas"};
vector<string> vec1 = { "a","c","be23","e123","1ca","x","aaa","z123","q","casd","asdas" };
for (const auto& item : vec) {
cout << item << std::ends;
}
cout << endl;
std::sort(vec.begin(),vec.end(),isShorter);
for (const auto& item : vec) {
cout << item << std::ends;
}
cout << endl;
std::stable_sort(vec1.begin(), vec1.end(), isShorter);
for (const auto& item : vec1) {
cout << item << std::ends;
}
cout << endl;
10.12
struct Sales_data
{
friend istream& input(istream& temp_input, Sales_data& data);
friend ostream& print(ostream& temp_cout, const Sales_data& data);
friend Sales_data add(Sales_data &data1, const Sales_data & data2);
private:
string bookNo;
unsigned units_sold = 0;
double revenue = { 0 };
public:
string isbn() const { return bookNo; };
Sales_data& combine(const Sales_data&);
inline double avg_price()const;
Sales_data(const string& isbn, unsigned u, double p) :bookNo(isbn), units_sold(u), revenue(u*p) {
cout << "三参数构造函数" << endl;
};
//构造函数
//1.委托构造函数
Sales_data() :Sales_data("", 0, 0) {
cout << "空参数构造函数" << endl;
};
//2.仅初始化isbn
Sales_data(const string& isbn) : Sales_data(isbn, 0, 0) {
cout << "isbn 构造函数" << endl;
};
//3.使用流来初始化
Sales_data(istream& temp_input) :Sales_data() {
input(temp_input, *this);
};
};
Sales_data& Sales_data::combine(const Sales_data& data) {
//bookNo = data.bookNo;
revenue += data.revenue;
return *this;
}
double Sales_data::avg_price() const {
return revenue / units_sold;
}
istream& input(istream& temp_input, Sales_data& data) {
double price = 0;
temp_input >> data.bookNo >> data.units_sold >> price;
data.revenue = price * data.units_sold;
return temp_input;
}
ostream& print(ostream& temp_cout, const Sales_data& data) {
temp_cout << data.bookNo << "," << data.units_sold << "," << data.revenue << endl;
return temp_cout;
}
Sales_data add(Sales_data &data1, const Sales_data & data2) {
Sales_data temp = data1;
data1.revenue += data2.revenue;
return temp;
}
vector<Sales_data> vec = {string("233"),string("123"),string("231"),string("xx123-123"),string("233")};
std::stable_sort(vec.begin(),vec.end(),compareIsbn);
for (const auto& item:vec) {
cout<<item.isbn()<<endl;
}
在这里使用了构造函数的隐式转换来构造Sales_data的向量。
10.13
bool over_five(const string& str) {
return str.size() >= 5 ? true : false;
}
vector<string> vec = { "a","c","be23","e123","1ca","1312x","aaa","z123","q","casd123","asdas" };
auto true_later_iter = std::partition(vec.begin(),vec.end(),over_five);
for (auto iter = vec.begin(); iter != true_later_iter;++iter) {
cout << *iter << endl;
}
10.3.2 lambda表达式
在上一节的练习中,需要打印长度大于等于5的元素,所在over_five()函数中,我们写入了一个硬编码5.
如果我们想要让这个大小可以灵活的变动,该怎么办呢?从函数的形参列表中传入吗,这是不行的,谓词的形参列表有着严格的参数个数固定,而且传入值的时候,是系统调用这个函数,不是我们自己控制调用的。
我们可以考虑率使用全局变量或者静态变量。但是还有更好的方法,可以让谓词的函数体中可以访问局部变量。
这种方法叫做lambda表达式。
在了解lambda表示之前,首先需要知道可调用对象,什么是可调用对象?
如果一个表达式或者一个对象可以使用调用运算符,也就是(),那么我们这个表达式或者对象是可调用的。
目前知道的可调用对象,只有函数和函数指针。初次之外还有lambda表达式和在重载了调用运算符的类。
lambda表达式由
捕获列表,形参列表,后置返回值类型,函数体构成。
[捕获列表](形参列表)->bool{函数体}
尾置返回类型除了在这里使用过之外,还有在返回函数指针时用过
auto fun_1()->int[10]{};
我们可以不写形参列表和返回值类型。如果没有返回值类型则根据return的结果来推断返回的类型,如果是一个空的return。那么返回值就是void。
书上说的是如果lambda表达式只有一个return语句,那么可以不加返回值类型,如果由多余的操作则必须加返回值类型,否则会造成编译报错,但是我在VS2017的编译环境下,lambda表达式中包含了其他操作,没有写返回值类型,一样可以编译通过
以下代码并不会报错
auto f = [&value](int j) {
j++;
value++;
if (value==0) {
return true;
}
//--value;
return false;
};
所以为了避免不同编译器的实现不一样,在写lambda表达式时,最好还是写上返回值类型
给lambda表达式传参和普通的函数是一样的。这里不再赘述。区别在于lambda表达式不能添加默认实参
重点是捕获列表,在捕获列表中写上的局部变量,可以在lambda表达式的函数体中使用。
对于静态变量和在函数体外定义的变量,不需要写在捕获列表中,可以直接访问,比如cin,cout就可以直接访问。
我们也可以写成=来捕获全部的局部变量,但是一般用啥就捕获啥,
int func(){
int value;
[value](){
value//可以访问
};
}
练习
10.14
auto f = [](int data1,int data2) ->int{
return data1 + data2;
};
cout<<f(1,2)<<endl;
10.15
int value = 233;
auto f2 = [value](int data)->int{
return value + data;
};
10.16,
10.18.
10.19
只是改变了第三步
void biggies(vector<string> &words, vector<string>::size_type sz) {
//1.排序去除重复元素
elimDups(words);
//2.按照长度排序,并维持字典序
std::stable_sort(words.begin(), words.end(), [](const string& str1, const string &str2)->bool {
return str1.size() < str2.size();
});
//3.得到第一个size()>sz的元素的位置
//10.16
// auto pos = std::find_if(words.begin(), words.end(), [sz](const string& str)->bool {
// return str.size() >= sz;
// });
//10.18
/*auto pos = std::partition(words.begin(), words.end(), [sz](const string& str) {
return str.size() < sz?true : false;
});*/
//10.19
auto pos = std::stable_partition(words.begin(), words.end(), [sz](const string& str) {
return str.size() < sz ? true : false;
});
//4.计算size>=sz的元素的数据
int num = words.end() - pos;
//5.打印size>=sz的元素
std::for_each(pos, words.end(), [](const string& str)->void {
cout << str << endl;
});
}
10.17
/*vector<Sales_data> vec = {string("233"),string("123"),string("231"),string("xx123-123"),string("233")};
std::stable_sort(vec.begin(), vec.end(), [](const Sales_data& data1,const Sales_data& data2)->bool {
return data1.isbn() < data2.isbn();
});
for (const auto& item:vec) {
cout<<item.isbn()<<endl;
}*/
10.3.3 lambda捕获和返回
下面的这段话非常的重要,需要理解。
1.当定义一个lambda表达式时,编译器将lambda表达式编译成了一个新的类类型。当我们给一个函数传递一个lambda表达式时,我们实际上是创建了一个新类型并创建了一个可调用对象,并将对象传给函数。
2.lambda表达式捕获列表中的值,相当于类的数据成员,这些数据成员在lambda表达式被创建时就被初始化,而不是调用时初始化
捕获列表中捕获变量的方式有很多种,分为显式和隐式。
也可以分为值捕获和引用捕获。
显式值捕获
值捕获要求函数外的局部变量必须时可以拷贝的,变量在创建lambda表达式的时候就被初始化,独立于外部的局部变量。
lambda默认值捕获的变量是不可以修改的,如果需要修改需要加上mutable关键字。
注意形参是可以改变的
int value = 1;
auto f = [value]()mutable->void{}
显式引用捕获
引用捕获其实就是在捕获列表中捕获变量时在变量的前面加上&。
int value = 1;
auto f = [&value]()->void{}
这样在lambda中修改变量,就会修改变量所指向的那个对象。
使用引用捕获时,需要注意在函数调用时必须确保引用的对象是存在的。 这是因为捕获列表是用来捕获局部变量的,而在函数调用结束后,局部变量的声明周期就结束了,但是lambda表达式可能在声明周期结束之后,还能调用。
所以在使用引用传递时,一定要确保在lambda函数调用时,引用的对象还存在。
除非必要(io类的对象不能拷贝,只能传递引用或者指针),一般不要再lambda表达式中,使用引用捕获。
隐式值捕获
可以在捕获列表中写上=,来表示用值传递的方式捕获lambda表达式中使用的局部变量。
我们可以一部分变量使用值传递,一部分一遍使用引用传递。
int a;
int b;
int c;
auto f = [=,&c](){
a;
b;
};
以上代码中c使用引用传递,而a和b使用值传递。
隐式引用捕获
我们也可以在捕获列表上使用&,表示在lambda中使用的局部变量为引用捕获
int a;
int b;
int c;
auto f = [&,c](){
a;
b;
};
如果某些变量我们想使用值捕获,则直接写上变量名字。
下表是捕获列表中参数的格式
练习
10.20
vector<string> vec = { "a","c","be23","e123","1ca","1312x","aaa","z123","q","casd123","asdas" };
auto num = std::count_if(vec.begin(), vec.end(), [](const string& item)->bool {
//vec[0] = "123";
return item.size() >= 5 ? true : false;
});
cout << num << endl;
10.21
使用引用捕获
int value = 3;
auto f = [&value]() {
value++;
if (value==0) {
return true;
}
//--value;
return false;
};
使用值捕获
int value = 3;
auto f = [value]()mutable->bool {
value++;
if (value==0) {
return true;
}
//--value;
return false;
};
10.3.4 参数绑定
lambda表达式可以为谓词传入额外的变量,一般情况下lambda表达式适用于那些只会写一次的函数。
如果某一个需求,频繁的需要调用额外的参数,而且这个函数到处都需要,那么我们很可能写下很多重复的lamba表达式。
这个时候把lambda表达式写成函数可以减少代码量,但是谓词对于传入的参数有着严格的规定。这个时候我们就需要用到bind。
bind定义在functional头文件中,是一个函数适配器。它可以将为一个函数绑定参数,并返回一个可调用对象。
使用bind的一般形式为
auto newCallable = bind(callable,arg_list)
这里的arg_list可以是占位符或者绑定的参数。
举个例子
bool compare_str_len_and_int_value(int value,size_t check_size) {
return value > check_size ? true : false;
}
vector<int> vec = {1,2,3,4,7,6,5,8,9,0};
string str = "hello";
auto iter = std::find_if(vec.begin(),vec.end(),std::bind(compare_str_len_and_int_value,_1,str.size()));
if (iter!=vec.end()) {
cout << *iter << endl;
}
函数compare_str_len_and_int_value包含两个参数,但是find_if要求一元谓词,所以我们可以使用bind来绑定参数。
因为vec中的每一个元素是算法传入的,所以我们给他一个占位符,而后续的check_size则绑定为一个具体的值。这样调用这个新对象时就只需要传入一个参数就行了。
这里的_1,_2表示是占位,表示这个参数不使用绑定,而下划线后面的名字表示这个参数是新对象的第几个参数。
占位placeholders定义在std::placeholders中。
因为_xxx表示的是,当前这个函数的第i的参数绑定在新对象的第xxx个位置,所以我们没有必要按照顺序写_1,_2.
所以下面代码中new_f。
在调用时
new_f(7,8,9);
映射到f中为
new_f(3,9,6,7,8);
int f(int a,int b,int c,int d,int e,int f) {
return 1;
}
auto new_f = std::bind(f,3,_3,6,_1,_2);
绑定引用参数
类似于lambda表达式的引用捕获,bind也可以绑定引用参数,但是不能直接使用,而是需要调用ref()函数。我们也可以使用cref()生成一个保存const引用的类。
int f(int& a){};
int a = 1;
bind(f,ref(a))
练习
10.22
vector<string> vec = { "a","c","be23","e123","1ca","1312x","aaa","z123","q","casd123","asdas" };
int sz = 6;
// 这里的占位符,_1,_2,_3表示原本函数的第x个参数,在新的对象中是第几个参数
auto num = std::count_if(vec.begin(), vec.end(), std::bind(world_len_compare,_1,sz));
cout << num << endl;
10.23
bind应该是能够接收无穷多个参数的,但是一般不会有人这么做,谁写的函数能写这么多参数。
10.24
vector<int> vec = {1,2,3,4,7,6,5,8,9,0};
string str = "hello";
auto iter = std::find_if(vec.begin(),vec.end(),std::bind(compare_str_len_and_int_value,_1,str.size()));
if (iter!=vec.end()) {
cout << *iter << endl;
}
10.25
void biggies(vector<string> &words, vector<string>::size_type sz) {
//1.排序去除重复元素
elimDups(words);
//2.按照长度排序,并维持字典序
std::stable_sort(words.begin(), words.end(), [](const string& str1, const string &str2)->bool {
return str1.size() < str2.size();
});
auto pos = std::stable_partition(words.begin(), words.end(), std::bind(compare_size,_1,sz));
//4.计算size>=sz的元素的数据
int num = words.end() - pos;
//5.打印size>=sz的元素
std::for_each(pos, words.end(), [](const string& str)->void {
cout << str << endl;
});
}