C++ lambda的重载
先说结论,lambda是不能重载的(至少到c++23依旧如此,以后会怎么样没人知道)。而且即使代码完全一样的两个lambda也会有完全不同的类型。
但虽然不能直接实现lambda重载,我们有办法去模拟。
在介绍怎么模拟之前,我们先看看c++里的functor是怎么重载的。
首先类的函数调用运算符是可以重载的,可以这样写:
struct Functor {
bool operator()(int i) const
{
return i % 2 == 0;
}
bool operator()(const std::string &s) const
{
return s.size() % 2 == 0;
}
};
在此基础上,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{};
现在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>{};
使用变长模板参数后就可以继承任意多的类了,然后再使用...
在类的内部逐个引入基类的函数调用运算符。
这样把继承的对象从普通的类改成lambda就可以模拟重载。但是怎么做呢,前面说了我们没法直接拿到lambda的类型,用decltype的话又会非常啰嗦。
答案是可以依赖c++17的新特性:CTAD。简单得说就是可以提前指定规则,让编译器从构造函数或者符合要求的构造方式里推导需要的类型参数。于是可以这样写:
template <typename... Ts>
Functor(Ts...) -> Functor<Ts...>;
箭头左边的是构造函数,右边的是推导出来的类型。
现在又有疑问了,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';
// Output:
// 2
// 3
// apocelipes!
// Hello, apocelipes!
}
有没有替代方案?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()
才是。这个方案除了啰嗦之外和上面靠继承的方案没有太大区别。
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重载,虽然一般不推荐使用多继承,但这里的多继承不会引发问题,而且可读性能获得很大提升,优势很明显,所以首选这种方案。
C++ lambda的重载的更多相关文章
- 【c++ Prime 学习笔记】第14章 重载运算与类型转换
14.1 基本概念 重载的运算符是特殊的函数:名字由关键字operator后接要定义的算符共同组成,也有返回类型.参数列表.函数体. 重载运算符函数的参数量与该算符作用的运算对象数量一样多 除重载调用 ...
- [C++ Primer] : 第10章: 泛型算法
概述 泛型算法: 称它们为"算法", 是因为它们实现了一些经典算法的公共接口, 如搜索和排序; 称它们是"泛型的", 是因为它们可以用于不同类型的元素和多种容器 ...
- lambda表达式与方法重载问题
笔者之前在学习Java8新特性的时候,最吸引我的就是lambda表达式,它无疑为Java函数编程提供了强有力的支持.lambda表达式的使用方法很简单,下面给出最简单的用法. // Interface ...
- 背后的故事之 - 快乐的Lambda表达式(一)
快乐的Lambda表达式(二) 自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜.它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能 ...
- Lambda
Lambda Lambda 表达式是一种可用于创建委托或表达式目录树类型的匿名函数. 通过使用 lambda 表达式,可作为参数传递或作为函数调用值返回的本地函数. Lambda 表达式对于编写 LI ...
- 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)
作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language- ...
- .NET中那些所谓的新语法之三:系统预定义委托与Lambda表达式
开篇:在上一篇中,我们了解了匿名类.匿名方法与扩展方法等所谓的新语法,这一篇我们继续征程,看看系统预定义委托(Action/Func/Predicate)和超爱的Lambda表达式.为了方便码农们,. ...
- 匿名方法与Lambda表达式
1.匿名方法 在学习委托时,我们知道委托实例至少要绑定一个方法才能使用,而调用委托实际上是调用了它所关联地方法.一般来说,需要定义一个与委托签名相符的方法,并使之与委托变量关联.如以下代码: Acti ...
- VS2012 Unit Test(Void, Action, Func) —— 对无返回值、使用Action或Func作为参数、多重载的方法进行单元测试
[提示] 1. 阅读文本前希望您具备如下知识:了解单元测试,了解Dynamic,熟悉泛型(协变与逆变)和Lambda,熟悉.NET Framework提供的 Action与Func委托.2.如果您对单 ...
- 19、lambda表达式树
一.定义: 表达式树又称为表达式目录树,以数据形式表示语言级代码.所有的数据都存储在树结构中,每个结点表示一个表达式(Expression). 二.要点: –Lambda表达式的参数类型可以忽略,因为 ...
随机推荐
- Redis高可用之战:主从架构
★ Redis24篇集合 1 主从模式介绍 在笔者的另外两篇文章 <Redis系列:RDB内存快照提供持久化能力>.<Redis稳定性之战:AOF日志支撑数据持久化>中,我们介 ...
- 新零售SaaS架构:客户管理系统的应用架构设计
客户管理系统的应用架构设计 应用层定义了软件系统的应用功能,负责接收用户的请求,协调领域层能力来执行任务,并将结果返回给用户,功能模块包括: 客户管理:核心功能模块,负责收集和更新客户信息,包括个人资 ...
- Java实现哈希表
2.哈希表 2.1.哈希冲突 冲突位置,把数据构建为链表结构. 装载因子=哈希表中的元素个数 / (散列表)哈希表的长度 装载因子越大,说明链表越长,性能就越低,那么哈希表就需要扩容,把数据迁移到新的 ...
- Linux C++ 连接 MySQL
安装MySQL 可以参考这篇文章<在Ubuntu上安装MySQL> 连接 具体可以看官方的MySQL参考手册 示例代码 #include<iostream> #include& ...
- 体验Semantic Kernel图片内容识别
前言 前几日在浏览devblogs.microsoft.com的时候,看到了一篇名为Image to Text with Semantic Kernel and HuggingFace的文章.这篇文章 ...
- 掌握 Spring IoC 容器与 Bean 作用域:详解 singleton 与 prototype 的使用与配置
在您的应用程序中,由 Spring IoC 容器管理的形成其核心的对象被称为 "bean".一个 bean 是由 Spring IoC 容器实例化.组装和管理的对象 这些 bean ...
- 【直播预告】HarmonyOS 极客松赋能直播第六期:产品创新从哪里来?
- Hive 查看表/分区更新时间
1.查看分区 hive> show partitions table_name; 2.查看分区更新时间 获取hdfs路径 hive> desc formatted table_name; ...
- 高云1N1开发板高云gowin软件使用教程
国产FPGA是最近几年起来的产品,具有性价比高特点.高云FPGA,很多用户都用在LED,电机控制,PLC设备上. 开发板子采用GW1N-LV1QN48C6/I5 FPGA器件.具有低功耗,瞬时启动,高 ...
- kkfileview搭建实战
kkfileview可以与nginx搭建的文件服务器配合实现预览工作,也可以通过自身的文件系统机制免搭建nginx文件服务器来实现预览工作. nginx 创建nginx # 创建初始容器,获得容器内部 ...