先说结论,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的重载的更多相关文章

  1. 【c++ Prime 学习笔记】第14章 重载运算与类型转换

    14.1 基本概念 重载的运算符是特殊的函数:名字由关键字operator后接要定义的算符共同组成,也有返回类型.参数列表.函数体. 重载运算符函数的参数量与该算符作用的运算对象数量一样多 除重载调用 ...

  2. [C++ Primer] : 第10章: 泛型算法

    概述 泛型算法: 称它们为"算法", 是因为它们实现了一些经典算法的公共接口, 如搜索和排序; 称它们是"泛型的", 是因为它们可以用于不同类型的元素和多种容器 ...

  3. lambda表达式与方法重载问题

    笔者之前在学习Java8新特性的时候,最吸引我的就是lambda表达式,它无疑为Java函数编程提供了强有力的支持.lambda表达式的使用方法很简单,下面给出最简单的用法. // Interface ...

  4. 背后的故事之 - 快乐的Lambda表达式(一)

    快乐的Lambda表达式(二) 自从Lambda随.NET Framework3.5出现在.NET开发者眼前以来,它已经给我们带来了太多的欣喜.它优雅,对开发者更友好,能提高开发效率,天啊!它还有可能 ...

  5. Lambda

    Lambda Lambda 表达式是一种可用于创建委托或表达式目录树类型的匿名函数. 通过使用 lambda 表达式,可作为参数传递或作为函数调用值返回的本地函数. Lambda 表达式对于编写 LI ...

  6. 深入理解Java 8 Lambda(语言篇——lambda,方法引用,目标类型和默认方法)

    作者:Lucida 微博:@peng_gong 豆瓣:@figure9 原文链接:http://zh.lucida.me/blog/java-8-lambdas-insideout-language- ...

  7. .NET中那些所谓的新语法之三:系统预定义委托与Lambda表达式

    开篇:在上一篇中,我们了解了匿名类.匿名方法与扩展方法等所谓的新语法,这一篇我们继续征程,看看系统预定义委托(Action/Func/Predicate)和超爱的Lambda表达式.为了方便码农们,. ...

  8. 匿名方法与Lambda表达式

    1.匿名方法 在学习委托时,我们知道委托实例至少要绑定一个方法才能使用,而调用委托实际上是调用了它所关联地方法.一般来说,需要定义一个与委托签名相符的方法,并使之与委托变量关联.如以下代码: Acti ...

  9. VS2012 Unit Test(Void, Action, Func) —— 对无返回值、使用Action或Func作为参数、多重载的方法进行单元测试

    [提示] 1. 阅读文本前希望您具备如下知识:了解单元测试,了解Dynamic,熟悉泛型(协变与逆变)和Lambda,熟悉.NET Framework提供的 Action与Func委托.2.如果您对单 ...

  10. 19、lambda表达式树

    一.定义: 表达式树又称为表达式目录树,以数据形式表示语言级代码.所有的数据都存储在树结构中,每个结点表示一个表达式(Expression). 二.要点: –Lambda表达式的参数类型可以忽略,因为 ...

随机推荐

  1. shk_to_bram

    Entity: shk_to_bram File: shk_to_bram.v Diagram Description Company: FpgaPublish Engineer: FP Create ...

  2. kingbaseES 优化之数据库瓶颈排查

    针对数据库的性能瓶颈排查方法分为两个层次1.实例级别性能问题排查 2.语句级别性能问题排查 实例级别 实例级别性能问题排查用来分析数据库实例整体是否存在性能瓶颈,然后根据排除出的疑似问题进行实例级别参 ...

  3. KingbaseES 等待事件之 - Client ClientWrite

    等待事件含义 Client:ClientWrite等待事件指数据库等待向客户端写入数据. 在正式业务系统中,客户端必然和数据库集群之间有数据交互,这里指的是数据接收,发送.数据库集群在向客户端发送更多 ...

  4. Scala 可变集合 mutable.Set

    1 package chapter07 2 3 import scala.collection.mutable 4 5 object Test07_MutableSet { 6 def main(ar ...

  5. Android---intent和startActivityForResult方法的使用---页面跳转和数据回传

    Android页面跳转和数据回传 今天我尝试用两个页面实现数据的传递和回传,出现了一些问题,把问题已经成功的案例总结如下: 具体是这样的: 有两个layout布局,两个activity.MainAct ...

  6. 讲讲百度地图API遇到的坑,石锤百度官方代码的错,解决SN校验失败

    这两天在做一个项目,用到了百度地图API,根据坐标获取具体位置,总结一下遇到的几个坑 本文基于最新的V3接口,网上好多要么是V2,要么根据地址获取坐标,本文是唯一一个最新的3,根据坐标获取位置的完整说 ...

  7. 关于Java 线程的运行状态

    首先需要说明的是,所指状态为JVM线程状态,而非操作系统线程状态.同一时间,一个线程只会存在于一种状态. 线程状态,enum State: 1.NEW 已创建,未运行. 2.RUNNABLE 线程于J ...

  8. #主席树,并查集#CodeChef Sereja and Ballons

    SEABAL 分析 考虑用并查集维护当前连续被打破的气球段,那么每次新增的区间就是 \([l_{x-1},x]\) 到 \([x,r_{x+1}]\) 的连接. 只要 \(l,r\) 分别满足在这之间 ...

  9. #状压dp#C 计划带师

    分析 状压dp显然,主要是字典序的问题, 考虑初态终态转换就可以保证字典序最小了 代码 #include <cstdio> #include <cctype> #include ...

  10. 一文弄懂EnumMap和EnumSet

    目录 简介 EnumMap 什么时候使用EnumMap EnumSet 总结 一文弄懂EnumMap和EnumSet 简介 一般来说我们会选择使用HashMap来存储key-value格式的数据,考虑 ...