一、Lambda表达式引入泛型
在c++11中引入lambda表达式后,确实是非常好用。但这里有一个问题,lambda表达式无法使用泛型,必须给出指定的类型。这就感官上不符合c++的开发形式,所以在c++14中引入了泛型,也就是可以使用auto做为参数的类型,通过自动推导来确定数据的类型。而在c++20后,更进一步引入模板参数,使得整个泛型推广到了和普通模板编程一致。
这也符合c++语言保持风格一致的指导思想,在这种情况下就不用记各种各样的特殊形式了。
二、具体的应用例程
先看一下在c++14中的泛型应用方式:
#include <iostream>
void TestAutoL(int a,int b)
{
auto getData = [](auto i, auto j)->auto {
return i + j;
};
int t = getData(a,b);
std::cout << "getData value is:" << t << std::endl;
}
//早期的版本c++17前,下面可能编译不通过
auto Get(auto x, auto y)
{
return x + y;
}
int main()
{
int x = Get(1,3);
TestAutoL(2,3);
return 0;
}
再看一下在c++20中的应用方式 :
template<typename T>
int foo1(T t)
{
return t;
}
template <typename... Args>
int foo(Args ...args)
{
return (foo1(args) + ...);
}
void TestCpp20(std::string a,std::string b)
{
auto f1 = []<typename T, typename N>(T t, N n)-> T {
return t + n;
};
auto f2 = []<typename ...T>(T && ...args) {
return foo(std::forward<T>(args)...);
};
std::string s = f1(a,b);
std::cout << "string is:" << s << std::endl;
int d = f2(1,2,3,6);
std::cout << "result is:" << d << std::endl;
}
int main()
{
std::string s1 = "123";
std::string s2 = "321";
std::string ss = s1 + s2;
TestCpp20(s1,s2);
return 0;
}
通过上面的对比,是否对lambda的泛型编程有了进一步形象的理解呢。
三、分析
在c++14中,其实是使用了一种取巧的泛型实现方式,通过auto来推导类型。而在同样的c++14中,直接写函数则无法实现类似功能(不过更新的版本则支持了,理论上是一样的),这应该只是一种尝试,这种情况可以认为是一种泛型中的特化,可以把auto当成一种具体的类型,只是这种类型需要简单推导一下。它更类似于下面的代码:
template<typename T, typename U>
auto operator()(T t, U u) const {return t + u;}
类似于仿函数的处理,或者说是闭包的一种处理方式。
在c++20中,lambda表达式的应用更加丰富,除了可以引入模板泛型,也可以实现更复杂的功能(比如做为模板参数),看例子:
#pragma once
#include <iostream>
template<typename... Ts> struct Animal : Ts... { using Ts::operator()...; };
template<typename... Ts> Animal(Ts...)->Animal<Ts...>;
struct Pig { void display() { std::cout << "is pig" << std::endl; } };
struct Horse { void display() { std::cout << "is horse"<< std::endl; } };
// 定义
static constexpr auto Factory = Animal{
[]<typename T>(const T& t) { return new T; }
};
void TestFactory()
{
auto pig = Factory(Pig{});
pig->display();
}
int main()
{
TestFactory();
return 0;
}
其实在c++的文档中有类似的代码前面也提到过:
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
上面一行使用了可变参数模板,使得overloaded可以继承自多个Lambda。其次使用了Using-declaration,以防止重载之时产生歧义。
下面一行则使用了C++17的CTAD(Class Template Argument Deduction),以推导出overloaded的类型。
这样做的目的,就是通过CTAD为overloaded添加一个用户自定义的类型推导指引,从而让编译器可以推导lambda的类型,进而可以创建出overloaded类型的对象。
那么可以进一步将抽象:
class Dog
{
public:
Dog() { }
void display() { std::cout << "this is Dog" << std::endl; }
};
class BigDog :public Dog
{
public:
BigDog() { }
void display() { std::cout << "this is BitDog" << std::endl; }
};
class Cow
{
public:
Cow() { }
void display() { std::cout << "this is Cow" << std::endl; }
};
class BigCow :public Cow
{
public:
BigCow() { }
void display() { std::cout << "this is BitCow" << std::endl; }
};
template<typename... Ts> struct AbstractFactory : Ts... { using Ts::operator()...; };
template<typename... Ts> AbstractFactory(Ts...)->AbstractFactory<Ts...>;
template < class T, class U>
concept IsAbstractAnimal = std::same_as<T, U>;
//此处其实可以更好的使用same_as,比如继承的处理
template <typename T>
static constexpr auto AbFactory = AbstractFactory{
[]<typename T>(const T&t)requires IsAbstractAnimal<T, Dog> { return new BigDog; },
[]<typename T>(const T&t)requires IsAbstractAnimal<T, Cow> { return new BigCow; },
};
void TestAbFactory()
{
auto dog = AbFactory<Dog>(Dog{});
dog->display();
auto cow = AbFactory<Cow>(Cow{});
cow->display();
}
int main()
{
TestAbFactory();
return 0;
}
上面的工厂换个名字就可以实现不同的类型创建,现在大狗大牛,也可以有小狗小牛,花狗花牛等等。只要把工厂名字改一下,concepts改一下就OK了。同样,在原来的一些STL中,需要传入仿函数的,有的不能使用lambda表达式,比如智能指针里的删除器,现在就可以了,在c++20中,可以直接在定义里使用。类似于下面:
my_unique_ptr<int,[](int* val_ptr) {
deletep(val_ptr);
}> my_uptr(new int(val));
c++中的技术可以不断的在标准进步的前提完善,就看实际应用的场景了。
四、总结
从这个lambda表达式可以看出来,其实c++的标准不断统一着编程风格,而且这种风格趋向于更容易理解和更容易分析对比的方向上前进。个人觉得这是一种非常让人舒服的发展方向,一如前面的SNIFAE逐渐被concepts替代,都是这种情况。虽然对c++这门语言来说,这是一种比较难于实现的过程,但只要朝着这个方向前进,就会不断赢得更多的开发人员的支持。