可变参数模板
我们之前就接触过函数的可变参数, 比如说c语言的printf函数和scanf函数,这两个就是经典的可变参数函数, c语言printf的底层是指针数组实现的, 把参数存到那个数组里, 然后遇到可变参数就是去解析那个数组里的内容.
//Args是一个模板参数包, args是一个函数形参参数包
//声明一个参数包Args... args, 这个参数包可以包含0到任意个模板参数
template<class ...Args>
void ShowList(Args... args)
{
}
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, 3);
ShowList(1, 2.2, "x", 3);
return 0;
}
如果要计算参数包的大小, 需要用sizeof..., 这个用法是固定的:
如果想要解析参数包的内容, 首先可能会想到数组式地访问, 但是不支持这样访问:
想要访问模板参数包的内容有两种方法, 第一种递归函数方式展开参数包:
void _ShowList()
{
cout << endl;
}
//编译时的递归推演
//第一个模板参数依次解析获取参数值
template<class T, class ...Args>
void _ShowList(const T& val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template<class ...Args>
void ShowList(Args... args)
{
//cout << sizeof...(args) << endl;
_ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, 3);
ShowList(1, 2.2, "x", 3);
return 0;
}
第二种逗号表达式展开参数包:
数组里面添加了三个点, 这个就表示数组在推断的时候需要把这个参数包进行展开, 展开的空间为多大, 这个数组的大小就是多大:
template<class T>
void PrintArg(const T& t)
{
cout << t << " ";
}
template<class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args),0)... };
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, 3);
ShowList(1, 2.2, "x", 3);
return 0;
}
上面的写法其实也用不到逗号表达式, 直接这样写也可以:
emplace
template <class... Args>
void emplace_back (Args&&... args);
那么相对insert和emplace系列接口的优势在哪里呢?
首先插入的参数是一个的话, 两者其实没有什么区别:
这样写会有一些区别, 但只是形式上有一些区别, 实际也没什么区别:
再次运行:
对于前两种情况不会出现这种问题原因是, push_back的value_type可以明确确定val的类型就是test::string, 而emplace_back只有一个参数也是test::string类型的, 所以也可以比较明确地推断出参数是test::string类型的.
而这种情况就不一样了:
再举一个例子, 如果push_back的话参数需要传一个pair因为value_type是写死的,需要传一个pair过去, 而emplace_back直接传两个参数进去, 让它直接去构造就可以了, 这里用string的构造间接表示pair的构造.
所以回归最开始的话题, 插入的参数是一个的话, push_back和emplace_back其实没有什么区别不管是传参方式还是底层效率, 因为移动构造的成本也很低, 可以说emplace_back是略微高效一点.
而多参数时效率其实也没什么区别, 但是emplace_back传参的方式更多了一点, 它不仅可以传左值和右值,还可以把参数一个个分开传:
可以再用自己的链表试一试:
int main()
{
test::list<pair<test::string, int>> lt;
cout << endl;
lt.push_back(make_pair("1111", 1));
lt.emplace_back("2222", 2);
return 0;
}
先实现一个emplace_back:
emplace_back要复用insert(实际上应该去复用emplace, push_back复用insert, emplace_back应该复用emplace, 这里就先这样写), 需要把函数参数包传给insert, 所以insert也要实现一个可变参数包版的:
insert里创建结点要用到new需要把参数包向下传, 所以node的构造函数也要实现一个可变参数包版的:
运行:
所以参数包其实有的时候不用去解析, 最终是一层一层往下传, 可变参数包直接被使用了.
lambda表达式
为什么会有lambda表达式
c语言中有函数指针, 它可以让我们传入函数作为参数, 但是函数指针在一些比较复杂的情况时会变得很难理解. 为了解决这个问题, C++就提出来了仿函数, 它可以大大的提高函数指针的可读性, 比如常用的sort函数:
在C++98中, 如果想要对一个数据集合中的元素进行排序, 可以使用std::sort方法:
#include <algorithm>
#include <functional>
int main()
{
int array[] = { 4,1,8,5,3,7,0,9,2,6 };
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
如果待排序元素为自定义类型, 需要用户定义排序时的比较规则:
#include <algorithm>
struct Goods
{
string _name; // 名字
double _price;// 价格
int _evaluate;// 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
ostream& operator<<(ostream& out, const Goods& g)
{
out << g._name << " "<< "评价" << g._evaluate << " " << "价格" << g._price << endl;
return out;
}
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
for (auto e : v)
cout << e;
cout << "---------------------------" << endl;
sort(v.begin(), v.end(), ComparePriceGreater());
for (auto e : v)
cout << e;
}
但随着C++语法的发展, 人们开始觉得上面的写法太复杂了, 每次为了实现一个algorithm算法, 都要重新去写一个类, 如果每次比较的逻辑不一样, 还要去实现多个类, 特别是相同类的命名, 这些都给编程者带来了极大的不便, 因此, 在C++11语法中出现了Lambda表达式.
lambda表达式语法
lambda表达式书写格式:
lambda表达式各部分说明
注意:
举几个例子:
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[] {};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=] {return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c) {b = a + c; };
fun1(10);
cout << a << " " << b << endl;//3 13
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int {return b += a + c; };
cout << fun2(10) << endl;
// 复制捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
使用lambda表达式之后之前按照Goods的价格排序的代码就变成下面这个样子:
#include <algorithm>
#include <functional>
struct Goods
{
string _name; // 名字
double _price;// 价格
int _evaluate;// 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
ostream& operator<<(ostream& out, const Goods& g)
{
out << g._name << " "<< "评价" << g._evaluate << " " << "价格" << g._price << endl;
return out;
}
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), [](const Goods& g1, Goods g2) ->bool {return g1._price < g2._price; });
for (auto e : v)
cout << e;
cout << "---------------------------" << endl;
sort(v.begin(), v.end(), [](const Goods& g1, Goods g2) ->bool {return g1._price < g2._price; });
for (auto e : v)
cout << e;
}
sort的第三个参数传lambda表达式即可:
这里可能存在一个疑问, lambda表达式为什么可以直接传递使用呢?
如果我想接收这个lambda表达式返回的匿名对象呢?
可以用auto接收, 不用管返回值的类型, auto会自动推导:
int main()
{
auto fun1 = [](const string& s) {cout << s << endl; };
fun1("hello fun1");
//函数体夜可以写成多行, 但一般lambda表达式不会太长
auto fun2 = [](const string& s)
{
cout << s << endl;
return 0;
};
fun2("hello fun2");
return 0;
}
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用, 以及使用的方式传值还是传引用.
这是一个简单的交换两个值的lambda函数:
现在如果我只想对main函数里的x和y进行交换呢? 可以用捕获列表进行捕获:
int main()
{
int x = 1, y = 2;
auto f1 = [x,y]()
{
int tmp = x;
x = y;
y = tmp;
};
f1();
cout << x << " " << y << endl;
return 0;
}
int main()
{
int x = 1, y = 2;
auto f1 = [x,y]()mutable
{
int tmp = x;
x = y;
y = tmp;
};
f1();
cout << x << " " << y << endl;
return 0;
}
因为这里父作用域只有两个变量, 所以也可以直接输入一个&, 引用传递捕捉所有父作用域中的变量.
那传引用捕捉这里的mutable可以去掉吗? 按照上面的理解是不能去掉的, 但是实际可以去掉.
最开始的捕获说明部分有提到捕捉this, 捕捉this是什么?
class AA
{
public:
void func()
{
auto f1 = [this]()
{
cout << a1 << endl;
cout << a2 << endl;
};
f1();
}
private:
int a1 = 1;
int a2 = 1;
};
int main()
{
AA a;
a.func();
return 0;
}
直接用赋值符号捕捉也会默认捕捉this, 因为this实际也在父作用域中.
如果直接捕捉a1和a2呢? 可以看到报错了, 因为a1和a2并不在父作用域中, 而且还没有捕捉this指针, 也无法访问a1和a2, 因为类内访问成员变量默认都是通过this访问.
注意:
void (*PF)();
int main()
{
auto f1 = [] {cout << "hello world" << endl; };
auto f2 = [] {cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
函数对象与lambda表达式
可以把lambda表达式的返回值类型打印出来看一看:
从使用方式上来看, 函数对象与lambda表达式完全一样.
class Rate
{
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lambda
auto r2 = [=](double monty, int year)->double {return monty * rate * year;};
r2(10000, 2);
return 0;
}
包装器
function包装器
function包装器, 也叫作适配器. C++中的function本质是一个类模板, 也是一个包装器.
那么我们来看看, 我们为什么需要function呢?
ret = func(x);
上面func可能是什么呢, func可能是函数名?函数指针? 函数对象(仿函数对象)? lamber表达式对象? 这几个可调用对象其实各有自己的缺陷:
先分别用函数,仿函数,lambda表达式实现交换:
//函数
void Swap_func(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
//仿函数
struct Swap
{
void operator()(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
};
int main()
{
//lambda表达式
auto Swaplambda = [](int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
};
}
然后再用包装器分别对它们进行包装:
在此基础上就可以让function包装器与map的配合使用:
除此之外, 函数指针, 函数对象(仿函数对象), lamber表达式, 这些都是可调用的类型, 如此丰富的类型, 可能会导致模板的效率低下!
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
// 函数名
double f(double i)
{
return i / 2;
}
// 函数对象
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl << endl;
return 0;
}
我们知道static修饰的变量是存放在静态区的它不会随着函数的结束生命周期就终止, 所以我们每次调用函数的时候都可以看到它的值在不断的累加并且地址也是一样的.
但是如果模板实例化出来多个函数呢? 由于静态变量是在函数第一次运行的时候进行创建, 而如果模板实例化出来多个函数, 所以每个函数的静态成员变量地址就是不一样的:
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
// 函数名
double f(double i)
{
return i / 2;
}
// 函数对象
struct Functor
{
double operator()(double d)
{
return d / 3;
}
};
int main()
{
// 函数名
function<double(double x)> f1 = f;
cout << useF(f1, 11.11) << endl<<endl;
// 函数对象
function<double(double x)> f2 = Functor();
cout << useF(f2, 11.11) << endl << endl;
// lamber表达式
function<double(double x)> f3 = [](double d)->double { return d / 4; };
cout << useF(f3, 11.11) << endl << endl;
return 0;
}
除此之外, 包装器还可以包装一些类的成员函数, 但是注意, 在包装类中函数的时候需要指定作用域, 并且如果包装的是非静态成员函数得在作用域的前面加上&, 如果是静态成员函数则加不加均可, 比如:
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 成员函数取地址, 比较特殊, 要加一个类域和&
//静态成员函数加不加&均可
function<int(int, int)> f1 = Plus::plusi;
function<int(int, int)> f1 =& Plus::plusi;
cout << f1(1, 2) << endl;
//非静态成员函数需要加&
function<double(double, double)> f2 = &Plus::plusd;
Plus ps;
cout << f2(1.1, 2.2) << endl;
return 0;
}
但是这里依然编译不通过, 通过报错信息可以看到这里类型不匹配, 模板参数少了一个Plus*类型的参数:
类的成员函数还有一个默认的this指针需要传, 加上之后编译通过:
也可以这样:
能不能每次不传这个第一个参数?
第一种方法我们可以创建一个该类型的对象, 然后通过lambda表达式来捕捉这个对象, 最后调用里面的函数来实现同样的功能, 那么这里的代码就如下:
int main()
{
Plus plus;
function<int(int, int)> f =
[&plus](double x, double y)->double {return plus.plusd(x, y); };
cout<<f(1.1, 2.2)<<endl;
return 0;
}
第二种方法可以用bind
bind
std::bind函数定义在头文件中, 是一个函数模板, 它就像一个函数包装器(适配器), 接受一个可
调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表.
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
一般而言, bind有两个用途:
可以将bind函数看作是一个通用的函数适配器, 它接受一个可调用对象, 生成一个新的可调用对
象来“适应”原对象的参数列表.
调用bind的一般形式: auto newCallable = bind(callable, arg_list);
其中, newCallable本身是一个可调用对象, arg_list是一个逗号分隔的参数列表, 对应给定的
callable的参数. 当我们调用newCallable时, newCallable会调用callable, 并传给它arg_list中
的参数.
arg_list中的参数可能包含形如_n的名字, 其中n是一个整数, 这些参数是“占位符”, 表示
newCallable的参数, 它们占据了传递给newCallable的参数的“位置”. 数值n表示生成的可调用对
象中参数的位置:_1为newCallable的第一个参数, _2为第二个参数, 以此类推.
int Sub(int a, int b)
{
return a - b;
}
int main()
{
function<int(int, int)> f1 = Sub;
cout << f1(10, 5) << endl;
// 调整参数顺序, 第一个参数和第二个参数位置互换
function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
cout << f2(10, 5) << endl;
// 调整参数个数,有些参数可以bind时写死
function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
cout << f3(5) << endl;
return 0;
}
之前的plus类可以试着把第一个参数绑定, 这样每次调用就不需要传一个对象了:
可以查看一下绑定之后的f3的类型:
int Sub(int a, int b)
{
return a - b;
}
int main()
{
// 调整参数个数,有些参数可以bind时写死
function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
cout <<"第一个参数绑定为20后: " << f3(5) << endl;
cout << typeid(f3).name() << endl;
return 0;
}
如果用auto接受bind的返回值:
int Sub(int a, int b)
{
return a - b;
}
int main()
{
// 调整参数个数,有些参数可以bind时写死
/*function<int(int)> f3 = bind(Sub, 20, placeholders::_1);*/
auto f3 = bind(Sub, 20, placeholders::_1);
cout <<"第一个参数绑定为20后: " << f3(5) << endl;
cout << typeid(f3).name() << endl;
return 0;
}
如果有多个参数能不能只绑定中间的参数呢?
void func(int a, int b, int c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
function<void(int, int)> f1 = bind(func, placeholders::_1, 100, placeholders::_2);
f1(1, 3);
return 0;
}