Kano编程之旅------C++设计习语--Lamaba表达式重载
C++设计习语
|15.Lamaba重载|
一.Lamba表达式
1.定义
[捕获列表] (函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}
(1)lambda表达式,是一个匿名函数(即没有函数名的函数)。
Lambda表达式基于数学中的λ演算得名。lamdba表达式有输入/出参数,返回值,函数体,唯独没有函数名,所以lambda表达式不能通过类型名来显示声明对象。但是可以使用auto和类型推导
(2)捕获列表是 Lambda 表达式最有意思的地方,这里重点介绍下捕获列表。
捕获列表,用来说明外部变量的访问方式,外部变量访问方式说明符可以是 = 或 & ,表示函数体中用到的、定义在外面的变量在函数体中是否允许被改变。= 表示值传递,不允许改变。& 表示引用传递,允许改变。
包括下面几种形式:
[ ] 表示不捕获任何变量
[=] 表示按值传递的方法捕获父作用域的所有变量
[&] 表示按引用传递的方法捕获父作用域的所有变量
[=, &a] 表示按值传递的方法捕获父作用域的所有变量,但按引用传递的方法捕获变量a
[&, a] 表示按引用传递的方法捕获父作用域的所有变量,但按值传递的方法捕获变量a
2.示例
auto fnIsEven = [](int value) -> bool {
return (0 == value % 2);
};
auto res4 = std::find_if(vec.begin(), vec.end(), fnIsEven);
auto res5 = std::find_if(vec.begin(), vec.end(), [](int value) {
return(0 == value % 2);
});
二.Lambda的重载
1.定义
先说结论,lambda是不能重载的(至少到c++23依旧如此)。而且即使代码完全一样的两个lambda也会有完全不同的类型。
但虽然不能直接实现lambda重载,我们有办法去模拟。
在介绍怎么模拟之前,我们先看看c++里的functor是怎么重载的。
(1)首先类的函数调用运算符是可以重载的,可以这样写:
struct Functor {
bool operator()(int i) const
{
return i % 2 == 0;
}
bool operator()(const std::string &s) const
{
return s.size() % 2 == 0;
}
};
(2)在此基础上,c++11还引入了using的新用法,可以把基类的方法提升至子类中,子类无需手动重写就可直接使用这些基类的方法:
struct IntFunctor {
bool operator()(int i) const
{
return i % 2 == 0;
}
};
struct StrFunctor {
bool operator()(const std::string &s) const
{
return s.size() % 2 == 0;
}
};
struct Functor: IntFunctor, StrFunctor {
// 不需要给出完整的签名,给出名字就可以了
// 如果在基类中这个名字已经有重载,所有重载的方法也会被引入
using IntFunctor::operator();
using StrFunctor::operator();
};
auto f = Functor{};
(3)现在Functor可以直接使用bool operator()(const std::string &s)和bool operator()(int i)了。
现在可以看看怎么模拟lambda重载了:我们知道c++标准要求编译器把lambda转换成类似上面的Functor的东西,因此也能使用上面的办法模拟重载。
但还有两个致命问题:第一是需要写明需要继承的lambda的类型,这个当然除了模板之外是做不到的;第二是继承的基类的数量得明确给出这限制了灵活性,但可以用c++11添加的新特性——变长模板参数来解决。
解决上面两个问题其实很简单,方案如下:
template <typename... Ts>
struct Functor: Ts...
{
using Ts::operator()...;
};
auto f = Functor<StrFunctor, IntFunctor>{};
(4)使用变长模板参数后就可以继承任意多的类了,然后再使用...在类的内部逐个引入基类的函数调用运算符。
这样把继承的对象从普通的类改成lambda就可以模拟重载。但是怎么做呢,前面说了我们没法直接拿到lambda的类型,用decltype的话又会非常啰嗦。
答案是可以依赖c++17的新特性:CTAD。简单得说就是可以提前指定规则,让编译器从构造函数或者符合要求的构造方式里推导需要的类型参数。于是可以这样写:
template <typename... Ts>
Functor(Ts...) -> Functor<Ts...>;
(5)箭头左边的是构造函数,右边的是推导出来的类型。
现在又有疑问了,Functor里不是没定义过任何构造函数吗?是的,正是因为没有定义,使得Functor符合条件成为了“聚合”(aggregate)。“聚合”可以做聚合初始化,形式类似:聚合{基类1初始化,基类2初始化, ...,成员变量1的值,成员变量2的值...}。
作为一种符合要求的初始化方式,也可以使用CTAD,但形式上会用圆括号包起来导致看着像构造函数。另外对于聚合,c++20会自动生成和上面一样的CTAD规则无需再手写。
现在把所有代码组合起来:
template <typename... Ts>
struct Functor: Ts...
{
using Ts::operator()...;
};
int main()
{
const double num = 2.0;
auto f = Functor{
[](int i) { return i+1; },
[&num](double d) { return d+num; },
[s = std::string{}](const std::string &data) mutable {
s = data + s;
return s;
}
};
std::cout << f(1) << '\n';
std::cout << f(1.0) << '\n';
std::cout << f("apocelipes!") << '\n';
std::cout << f("Hello, ") << '\n';
auto f2 = [](int i) { return i + 1; };
std::cout << f2(3) << '\n';
// Output:
// 2
// 3
// apocelipes!
// Hello, apocelipes!
}
(6)有没有替代方案?c++17之后是有的,可以利用if constexpr或者if consteval对类型分别进行处理,编译器编译时会忽略其他分支,实际上这不是重载,但实现了类似的效果:
int main()
{
auto f = []/*template*/ <typename T>(T t) {
if constexpr (std::is_same_v<T, int>) {
return t + 1;
}
else if constexpr (std::is_same_v<T, std::string>) {
return "Hello, " + t;
}
else {
return t;
}
};
std::cout << f(1) << '\n';
std::cout << f("apocelipes") << '\n';
std::cout << f(1.2) << '\n';
// Output:
// 2
// Hello, apocelipes
// 1.2
}
要注意的是这里的f本身并不是模板,f的operator()才是。这个方案除了啰嗦之外和上面靠继承的方案没有太大区别。
2.lambda重载有啥用呢?目前一大用处是可以简化std::visit的使用:
std::variant<int, long, double, std::string> v;
// 对v一顿操作
std::visit(Functor{
[](int arg) { std::cout << arg << ' '; },
[](long arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; }
}, v);
这个场景中需要一个callable对象,同时需要它的调用运算符有对应类型的重载,在这里不能直接用模板,所以我们的模拟lambda重载派上了用场。
如果要我推荐的话,我会选择继承的方式实现lambda重载,虽然一般不推荐使用多继承,但这里的多继承不会引发问题,而且可读性能获得很大提升,优势很明显,所以首选这种方案。
三.延伸
1.C++聚合初始化--
如果你的代码中没有定义构造函数,而你依然能够成功编译和运行 String a("Hello");
那么你可能使用的是一个支持聚合初始化的编译器版本。这种情况下,C++ 标准允许聚合
类型使用这种形式的初始化,但这仅在特定条件下有效。
(1)根据 C++ 标准,如果一个类满足以下所有条件,则它是一个聚合类型:
所有成员都是 public。
没有用户提供的构造函数(即使默认构造函数也没有)。
没有虚函数。
没有私有或受保护的非静态数据成员。
没有基类和没有虚基类。
(2)在这种情况下,编译器会为这个类生成一个隐式的构造函数,使得你可以用花括号初始化列表或圆括号进行初始化。
示例代码
以下是一个没有显式定义构造函数但能够使用圆括号初始化的完整示例:
#include <iostream>
class String {
public:
const char* str;
// 注意:这里没有定义任何构造函数
// 编译器会自动生成一个隐式的默认构造函数
// 使得可以使用聚合初始化
char at(unsigned pos) {
return str[pos];
}
};
int main() {
// 使用隐式生成的构造函数初始化
String a("Hello");
std::cout << a.at(1) << std::endl; // 输出 'e'
return 0;
}
(3)解释
隐式默认构造函数:由于 String 类没有定义任何构造函数,编译器会生成一个隐式的默认构造函数。因为 String 满足聚合类型的条件(所有成员都是 public,没有用户提供的构造函数等),所以可以使用圆括号初始化。
初始化对象:String a("Hello"); 调用隐式生成的构造函数,并将字符串字面量 "Hello" 赋值给 str 成员。
访问成员函数:std::cout << a.at(1) << std::endl; 调用 at 成员函数,返回字符串 "Hello" 中索引为 1 的字符,即 e,并输出。
(4)总结
在没有显式定义构造函数的情况下,由于 String 类满足聚合类型的条件,编译器会生成一个隐式的构造函数,使得 String a("Hello"); 这种形式的初始化是合法的。这是因为编译器将字符串字面量 "Hello" 直接赋值给了 str 成员。
2.c++中constexpr的用法是什么
在C++中,constexpr关键字用于声明一个表达式或函数是在编译时计算的常量。它可以被用于常量表达式的定义、变量声明、函数声明等地方。
使用constexpr关键字可以将编译时常量的计算推迟到编译期间,从而提高程序的性能。它的主要用法包括以下几个方面:
(1)声明常量表达式:使用constexpr关键字可以将某个表达式声明为编译时常量。例如:
constexpr int size = 10;
(2)定义常量:使用constexpr关键字可以声明并定义一个编译时常量。例如:
constexpr int factorial(int n) {
return (n <= 1) ? 1 : (n * factorial(n - 1));
}
constexpr int result = factorial(5); // 在编译期间计算factorial(5)的结果并赋值给result
(3)用于数组大小:constexpr关键字可以用于声明数组的大小,要求数组大小必须是一个编译时常量。例如:
constexpr int size = 5;
int arr[size]; // 可以使用编译时常量作为数组大小
(4)用于模板参数:constexpr关键字可以用于模板参数,从而要求模板参数必须是一个编译时常量。例如:
template <int N>
void printArraySize() {
int arr[N];
std::cout << "Array size: " << N << std::endl;
}
constexpr int size = 10;
printArraySize<size>(); // 编译时展开模板,并将size作为常量传递给模板函数
总的来说,constexpr关键字的使用可以使得程序在编译期间进行更多的计算,提高程序的性能,并且在一些场景下可以避免运行时的开销。
3.std::is_same_v的解释和使用
#include <iostream>
using namespace std;
template <typename T>
int getValueType(T v)
{
if (std::is_same_v<T, int>)
{
return 1;
}
if (std::is_same_v<T, double>)
{
return 2;
}
if (std::is_same_v<T, char*>)
{
return 3;
}
}
int main() {
int a = 100;
double b = 1.1;
char* c;
cout << getValueType(a) << " " << getValueType(b) << " " << getValueType(c) << " " << endl;
return 0;
}