突破编程_C++_C++14新特性(泛型 Lambda 表达式)

1 C++11 的 Lambda 表达式回顾

C++11 引入了 Lambda 表达式,这是一种简洁的创建匿名函数对象的方式。Lambda 表达式特别适用于需要定义小型函数对象但又不想为其编写整个函数或类定义的场景。下面,我将详细讲解 C++11 中的 Lambda 表达式的各个部分和它们如何工作。

(1)Lambda 表达式的基本语法

Lambda 表达式的基本语法如下:

[capture](parameters) -> return_type { body }
  • [capture]:捕获子句,用于指定哪些外部变量可以在 Lambda 表达式的函数体中访问。
  • (parameters):参数列表,与普通函数的参数列表类似。
  • -> return_type:返回类型,指定 Lambda 表达式的返回类型。如果省略,编译器会根据函数体中的代码推断返回类型。
  • { body }:Lambda 表达式的函数体,包含 Lambda 函数的实现。

(2)捕获子句

捕获子句决定了哪些外部变量可以在 Lambda 表达式的函数体中访问。捕获子句可以是以下几种形式之一:

  • []:不捕获任何外部变量。
  • [=]:以值捕获所有外部变量。
  • [&]:以引用捕获所有外部变量。
  • [a, &b]:以值捕获 a,以引用捕获 b。

捕获的变量可以是 const 的,以禁止在 Lambda 表达式中修改它们。

(3)参数列表

Lambda 表达式的参数列表与普通函数的参数列表类似,用于定义 Lambda 函数的输入参数。

auto add = [](int x, int y) { return x + y; };

在这个例子中,add 是一个 Lambda 表达式,它接受两个整数参数 x 和 y,并返回它们的和。

(4)返回类型

Lambda 表达式的返回类型可以是显式指定的,也可以由编译器根据函数体中的代码推断。

auto is_positive = [](int x) -> bool { return x > 0; };

在这个例子中,Lambda 表达式 is_positive 的返回类型是 bool,它接受一个整数参数 x 并检查 x 是否大于 0。

(5)函数体

Lambda 表达式的函数体包含了 Lambda 函数的实现,就像普通函数的函数体一样。

auto greet = [](const std::string& name) {  
    std::cout << "Hello, " << name << "!" << std::endl;  
};

在这个例子中,greet 是一个 Lambda 表达式,它接受一个字符串参数 name 并打印一条问候消息。

(6)使用 Lambda 表达式

Lambda 表达式可以赋值给函数指针(如果 Lambda 没有捕获任何变量并且其返回类型可以与某个函数指针类型匹配)或者存储到任何接受函数对象的容器或算法中,比如 std::function、std::sort 等。

std::function<bool(int, int)> compare = [](int a, int b) { return a < b; };  
std::vector<int> vec = {4, 2, 5, 1, 3};  
std::sort(vec.begin(), vec.end(), compare);

在这个例子中,Lambda 表达式被赋值给 std::function 对象 compare,然后用作 std::sort 的比较函数来对向量 vec 进行排序。

(7)闭包

Lambda 表达式创建了一个闭包对象,该对象捕获了外部作用域中的变量。这些捕获的变量在 Lambda 表达式的生命周期内保持有效,并且可以通过值或引用在 Lambda 函数体中访问。

(8)注意事项

  • Lambda 表达式不能修改它所在作用域的局部变量(除非它们被显式捕获或以引用方式捕获)。
  • Lambda 表达式在捕获列表中使用 = 或 & 时,必须小心避免悬挂引用或意外的值复制。
  • Lambda 表达式在捕获大型对象时可能会导致性能问题,因为它们通常是通过值捕获的。

2 C++14 引入的泛型 Lambda 表达式

2.1 泛型 Lambda 表达式的基本概念

C++14 的泛型 Lambda 表达式是 C++11 Lambda 表达式的扩展,它允许在 Lambda 表达式的参数列表中使用自动类型推导(auto),从而创建出更加灵活和通用的 Lambda 函数对象。泛型 Lambda 表达式使得 Lambda 可以接受任意类型的参数,而不仅仅是预定义类型的参数。

(1)基本概念

在 C++14 之前,Lambda 表达式的参数类型需要明确指定。然而,在 C++14 中,你可以使用 auto 关键字作为 Lambda 表达式的参数类型,编译器会根据调用时的实际参数类型自动推导参数的类型。这种使用 auto 关键字的 Lambda 表达式就被称为泛型 Lambda 表达式。

(2)使用示例

下面是一个简单的泛型 Lambda 表达式的示例:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    // 泛型 Lambda 表达式,用于打印容器中的元素  
    auto printElement = [](const auto& elem) {  
        std::cout << elem << " ";  
    };  
  
    // 使用泛型 Lambda 表达式打印整数向量  
    std::vector<int> intVec = {1, 2, 3, 4, 5};  
    std::for_each(intVec.begin(), intVec.end(), printElement);  
    std::cout << std::endl;  
  
    // 使用泛型 Lambda 表达式打印字符串向量  
    std::vector<std::string> strVec = {"Hello", "World", "C++", "14"};  
    std::for_each(strVec.begin(), strVec.end(), printElement);  
    std::cout << std::endl;  
  
    return 0;  
}

上面代码的输出为:

1 2 3 4 5
Hello World C++ 14

在这个例子中,printElement 是一个泛型 Lambda 表达式,它接受一个 const auto& 类型的参数 elem。这意味着 elem 可以是任何类型,编译器会根据实际传递给 printElement 的参数类型自动推导 elem 的类型。因此,同一个 printElement Lambda 表达式可以用于打印整数向量和字符串向量中的元素。

2.2 泛型 Lambda 表达式与模板函数的区别

C++14 的泛型 Lambda 表达式与模板函数都是用来处理不同类型数据的方法,但它们在实现、语法、使用场景等方面存在一些重要的区别。

(1)实现方式

泛型 Lambda 表达式:

泛型 Lambda 表达式使用 auto 关键字在参数列表中表示参数的类型,让编译器根据实际的参数类型自动推导。这实际上是编译器在背后生成了一个内部模板类,从而实现了泛型。

auto lambda = [](const auto& x) { /* ... */ };

模板函数:

模板函数则通过显式地声明模板参数来定义函数,这些模板参数在函数调用时会被实际的类型参数所替代。

template<typename T>  
void function(const T& x) { /* ... */ }

(2)语法

泛型 Lambda 表达式:

泛型 Lambda 表达式的语法相对简洁,只需要在参数列表中使用 auto 关键字即可。

auto sum = [](const auto& a, const auto& b) { return a + b; };

模板函数:

模板函数需要显式地声明模板参数列表,并且需要在函数名之前指定模板参数。

template<typename T>  
T sum(const T& a, const T& b) { return a + b; }

(3)使用场景

泛型 Lambda 表达式:

泛型 Lambda 表达式主要用于那些需要匿名函数对象的场合,例如算法库中的函数对象、事件处理等。它们特别适合在局部范围内定义和使用,不需要为整个类或命名空间声明模板函数。

std::vector<int> v = {1, 2, 3, 4, 5};  
std::transform(v.begin(), v.end(), v.begin(), [](int x) { return x * 2; });

模板函数:

模板函数则更适用于那些需要在多个地方重复使用的泛型函数,例如通用的算法、数学运算等。模板函数可以被定义在头文件中,并在多个源文件中包含和使用。

template<typename T>  
T max(const T& a, const T& b) { return (a > b) ? a : b; }  
  
// 在其他源文件中使用  
int main() {  
    int m = max(3, 5);  
    double d = max(3.14, 2.71);  
    // ...  
}

(4)捕获列表

泛型 Lambda 表达式:

泛型 Lambda 表达式支持捕获列表,这意味着它们可以捕获其所在作用域中的局部变量,使得 Lambda 表达式能够访问和操作这些变量。

int x = 10;  
auto printX = [x](const auto& y) { std::cout << x << " " << y << std::endl; };

模板函数:

模板函数则不支持捕获列表,它们是完全独立的函数定义,无法访问其所在作用域中的局部变量。

(5)性能考虑

泛型 Lambda 表达式:

由于泛型 Lambda 表达式通常用于局部范围,并且可能只被调用几次,因此编译器可能不会对它们进行过度的优化。此外,由于它们可能涉及捕获列表和闭包,因此可能引入一些额外的性能开销。

模板函数:

模板函数在编译时会被实例化,并且由于它们通常用于多次调用的场景,编译器可能会对它们进行更深入的优化。模板函数通常具有更好的性能特性,特别是在处理大型数据集或进行复杂计算时。

3 泛型 Lambda 表达式的参数类型推导

(1)推导规则

在泛型 Lambda 表达式中,当参数类型被指定为 auto 时,编译器会根据以下几个规则来推导参数的实际类型:

  • 直接推导:如果传递给 Lambda 表达式的参数类型明确,编译器会直接使用该类型作为推导结果。

  • 引用折叠:如果推导出的类型是引用类型(例如 T& 或 T&&),并且 T 本身也是一个引用类型,那么会发生引用折叠。左值引用折叠成左值引用,右值引用折叠成右值引用。

  • 模板类型推导:编译器使用模板类型推导规则来确定 auto 的实际类型。这通常涉及到对传递给 Lambda 的实际参数进行模式匹配。

(2)推导示例

下面是一些示例,展示了泛型 Lambda 表达式的参数类型推导过程:

auto lambda1 = [](auto x) { /* ... */ }; // 推导规则应用于单个参数  
auto lambda2 = [](auto& x) { /* ... */ }; // 引用类型推导  
auto lambda3 = [](auto&& x) { /* ... */ }; // 转发引用(右值引用)推导  
  
int a = 10;  
const int b = 20;  
double c = 3.14;  
  
// 调用 lambda1,推导 x 的类型为 int  
lambda1(a);  
  
// 调用 lambda2,推导 x 的类型为 int&(左值引用)  
lambda2(a);  
  
// 调用 lambda2,推导 x 的类型为 const int&(左值引用到常量)  
lambda2(b);  
  
// 调用 lambda3,推导 x 的类型为 int&&(右值引用),但这里 a 是一个左值  
// 因此会发生左值到右值的转换(std::move),但通常不建议这样使用  
lambda3(a);  
  
// 调用 lambda3,推导 x 的类型为 double&&(右值引用),因为 c 是一个右值  
lambda3(std::move(c)); // 或者简单地传递一个临时对象,如 lambda3(double(3.14));

在上面的示例中,lambda1、lambda2 和 lambda3 分别展示了不同类型的参数推导。lambda1 的参数 x 被推导为传递给它的实际参数类型(这里是 int)。lambda2 的参数 x 是一个左值引用,因此它会被推导为对应参数的左值引用类型(这里是 int& 和 const int&)。lambda3 的参数 x 是一个转发引用(右值引用),它根据传递给它的参数是左值还是右值来推导类型。如果传递的是左值,它会推导为对应的左值引用类型;如果传递的是右值,它会推导为对应的右值引用类型。

(3)推导限制与注意事项

虽然泛型 Lambda 表达式的参数类型推导非常强大,但也存在一些限制和注意事项:

  • 非类型模板参数:对于非类型模板参数(如枚举、指针和整数类型),类型推导可能不适用或有限制。
  • 完美转发:当使用转发引用(auto&&)时,需要特别注意完美转发(Perfect Forwarding)的概念,以避免不必要的值类别改变。
  • 自动类型推导与显式类型指定:在某些情况下,显式指定 Lambda 表达式的返回类型和参数类型可能比使用自动类型推导更清晰和可靠。
  • 推导复杂性:复杂的类型推导可能导致代码难以理解和维护。因此,在使用泛型 Lambda 表达式时,应确保推导过程清晰明了,并避免过度依赖复杂的类型推导。

4 泛型 Lambda 表达式的应用

泛型 Lambda 表达式在容器中的应用非常广泛,它们为处理容器中存储的不同类型元素提供了极大的灵活性。在 C++ 标准库中,容器(如 std::vector、std::list、std::map 等)通常用于存储和管理数据。泛型 Lambda 表达式可以配合算法库(如 std::algorithm)一起使用,对容器中的元素进行各种操作,而无需关心元素的具体类型。

下面是一些泛型 Lambda 表达式在容器中的典型应用示例:

(1)遍历容器并操作元素

使用泛型 Lambda 表达式可以轻松地遍历容器并对每个元素执行操作。例如,假设我们有一个 std::vector,我们想要打印出其中的所有元素:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    std::vector<int> vec = {1, 2, 3, 4, 5};  
  
    // 使用泛型 Lambda 表达式遍历并打印元素  
    std::for_each(vec.begin(), vec.end(), [](const auto& element) {  
        std::cout << element << " ";  
    });  
    std::cout << std::endl;  
  
    return 0;  
}

上面代码的输出为:

1 2 3 4 5

(2)查找容器中的元素

泛型 Lambda 表达式也可以用于查找容器中满足特定条件的元素。例如,使用 std::find_if 算法:

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    std::vector<int> vec = {1, 2, 3, 4, 5};  
    int target = 3;  
  
    // 使用泛型 Lambda 表达式查找元素  
    auto it = std::find_if(vec.begin(), vec.end(), [target](const auto& element) {  
        return element == target;  
    });  
  
    if (it != vec.end()) {  
        std::cout << "Found " << target << " at position " << std::distance(vec.begin(), it) << std::endl;  
    } else {  
        std::cout << target << " not found" << std::endl;  
    }  
  
    return 0;  
}

上面代码的输出为:

Found 3 at position 2

(3)转换容器中的元素类型

使用泛型 Lambda 表达式和 std::transform 算法,可以轻松转换容器中元素的类型或执行其他操作:

#include <iostream>  
#include <vector>  
#include <algorithm>  
#include <iterator> // for std::back_inserter  
  
int main() {  
    std::vector<int> vec = {1, 2, 3, 4, 5};  
    std::vector<double> result;  
  
    // 使用泛型 Lambda 表达式转换元素类型并存储到新的容器中  
    std::transform(vec.begin(), vec.end(), std::back_inserter(result), [](const auto& element) {  
        return static_cast<double>(element) * 2.0;  
    });  
  
    // 打印转换后的结果  
    for (const auto& val : result) {  
        std::cout << val << " ";  
    }  
    std::cout << std::endl;  
  
    return 0;  
}

上面代码的输出为:

2 4 6 8 10

(4)排序容器中的元素

虽然 std::sort 算法本身不需要泛型 Lambda 表达式(因为它默认使用 operator< 进行比较),但你可以通过自定义比较函数或 Lambda 表达式来定义非标准的排序规则。这在处理自定义类型或需要非标准比较逻辑的场合特别有用。

#include <iostream>  
#include <vector>  
#include <algorithm>  
#include <string>  
  
struct Person {  
    std::string name;  
    int age;  
};  
  
int main() {  
    std::vector<Person> people = {  
        {"Alice", 30},  
        {"Bob", 25},  
        {"Charlie", 35}  
    };  
  
    // 使用泛型 Lambda 表达式按年龄排序  
    std::sort(people.begin(), people.end(), [](const auto& lhs, const auto& rhs) {  
        return lhs.age < rhs.age;  
    });  
  
    // 打印排序后的结果  
    for (const auto& person : people) {  
        std::cout << person.name << " " << person.age << std::endl;  
    }  
  
    return 0;  
}

上面代码的输出为:

Bob 25
Alice 30
Charlie 35

(5)移除容器中的元素

结合 std::remove_if 和 std::vector::erase 方法,你可以使用泛型 Lambda表达式来移除容器中满足特定条件的元素。std::remove_if 算法会将不满足条件的元素移动到容器的开始部分,并返回一个迭代器指向新逻辑末尾的位置,随后你可以使用 std::vector::erase 来移除剩余的元素。

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    std::vector<int> vec = {1, 2, 3, 4, 5, 2, 6, 2, 7};  
  
    // 使用泛型 Lambda 表达式移除所有值为 2 的元素  
    vec.erase(std::remove_if(vec.begin(), vec.end(), [](const auto& element) {  
        return element == 2;  
    }), vec.end());  
  
    // 打印移除元素后的结果  
    for (const auto& val : vec) {  
        std::cout << val << " ";  
    }  
    std::cout << std::endl;  
  
    return 0;  
}

上面代码的输出为:

1 3 4 5 6 7

在这个例子中,std::remove_if 使用了一个泛型 Lambda 表达式来检查每个元素是否等于 2。所有等于 2 的元素都会被移动到容器的末尾,并返回一个指向新逻辑末尾的迭代器。然后,vec.erase 被用来删除从返回迭代器到 vec.end() 的所有元素,从而有效地从容器中移除了所有值为 2 的元素。

(6)泛型 Lambda 表达式与自定义类型

泛型 Lambda 表达式在处理自定义类型时特别有用,因为你可以编写一个通用的 Lambda 表达式来处理多种不同的类型,而无需为每个类型编写特定的代码。例如,假设你有一个包含自定义类型的容器,并且你想要对这个容器中的每个对象执行相同的操作。通过使用泛型 Lambda 表达式,你可以编写一个通用的操作,而无需关心容器中对象的具体类型。

泛型 Lambda 表达式为容器操作提供了极大的灵活性和便利性。它们使得编写可重用、类型安全的代码变得更加容易,尤其是在处理多种不同数据类型的容器中。通过使用泛型 Lambda 表达式,可以编写更加通用和模块化的代码,减少重复劳动,并提高代码的可维护性。

  • 39
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值