函数对象、lambda表达式、function模板

一、函数对象

定义:任何定义了函数调用操作符的对象都是函数对象。

函数:

  • 函数可以使用()进行调用
  • 函数在参数传递时通常退化为函数指针

函数指针

  •  函数指针可以解引用调用:(*ptr)(...)参数列表
  • 函数指针也可以直接调用:ptr(...)参数列表

定义了operator()成员函数的对象

  • 语法和普通函数一致:obj(...);

函数对象是指重载了 operator() 的类实例,可以像函数一样被调用。下面是一个简单的函数对象实现示例:

#include <iostream>

// 简单的函数对象类
class Adder {
private:
    int base;  // 基数值
public:
    // 构造函数
    Adder(int b) : base(b) {}
    
    // 重载 operator()
    int operator()(int x) const {
        return base + x;
    }
};

int main() {
    // 创建函数对象实例,base 设为 5
    Adder add5(5);
    
    // 像函数一样调用
    std::cout << "5 + 3 = " << add5(3) << std::endl;  // 输出: 5 + 3 = 8
    std::cout << "5 + 7 = " << add5(7) << std::endl;  // 输出: 5 + 7 = 12
    
    // 也可以创建另一个基数的加法器
    Adder add10(10);
    std::cout << "10 + 4 = " << add10(4) << std::endl;  // 输出: 10 + 4 = 14
    
    return 0;
}

更通用的函数对象模板

如果需要更通用的实现,可以使用模板:

#include <iostream>

template <typename T>
class Multiplier {
private:
    T factor;
public:
    Multiplier(T f) : factor(f) {}
    
    T operator()(T x) const {
        return factor * x;
    }
};

int main() {
    Multiplier<int> times2(2);
    std::cout << "2 * 3 = " << times2(3) << std::endl;  // 输出: 2 * 3 = 6
    
    Multiplier<double> times0_5(0.5);
    std::cout << "0.5 * 7 = " << times0_5(7.0) << std::endl;  // 输出: 0.5 * 7 = 3.5
    
    return 0;
}

带状态的函数对象

函数对象可以保持状态,这是普通函数无法做到的:

#include <iostream>

class Counter {
private:
    int count;
public:
    Counter() : count(0) {}
    
    int operator()() {
        return ++count;
    }
    
    void reset() {
        count = 0;
    }
};

int main() {
    Counter counter;
    std::cout << counter() << std::endl;  // 输出: 1
    std::cout << counter() << std::endl;  // 输出: 2
    std::cout << counter() << std::endl;  // 输出: 3
    
    counter.reset();
    std::cout << counter() << std::endl;  // 输出: 1
    
    return 0;
}

二、lambda表达式

Lambda 表达式是 C++11 引入的一种简洁的匿名函数定义方式,可以用于创建函数对象。Lambda 表达式通常用于需要传递简单函数作为参数的场合,比如 STL 算法中的谓词或比较函数。

下面是一些简单的 lambda 表达式示例:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    // 示例 1: 简单的 lambda 表达式
    auto greet = []() {
        std::cout << "Hello, World!" << std::endl;
    };
    greet(); // 输出: Hello, World!

    // 示例 2: 带参数的 lambda 表达式
    auto add = [](int a, int b) {
        return a + b;
    };
    std::cout << "3 + 4 = " << add(3, 4) << std::endl; // 输出: 3 + 4 = 7

    // 示例 3: 捕获外部变量的 lambda 表达式
    int x = 10;
    auto multiply_by_x = [x](int y) {
        return x * y;
    };
    std::cout << "10 * 5 = " << multiply_by_x(5) << std::endl; // 输出: 10 * 5 = 50

    // 示例 4: 在 STL 算法中使用 lambda 表达式
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // 使用 lambda 表达式计算平方
    std::for_each(numbers.begin(), numbers.end(), [](int n) {
        std::cout << n * n << " ";
    });
    std::cout << std::endl; // 输出: 1 4 9 16 25 36 49 64 81 100

    // 示例 5: 捕获列表中使用引用
    int sum = 0;
    std::for_each(numbers.begin(), numbers.end(), [&sum](int n) {
        sum += n;
    });
    std::cout << "Sum of numbers: " << sum << std::endl; // 输出: Sum of numbers: 55

    // 示例 6: 捕获列表中使用 mutable
    int count = 0;
    auto increment = [&count]() mutable {
        count++;
    };
    increment();
    increment();
    std::cout << "Count: " << count << std::endl; // 输出: Count: 2

    return 0;
}

解释

  1. 基本语法:Lambda 表达式的基本语法是 [capture list](parameters) -> return type { body }

  2. 捕获列表:用于捕获外部作用域中的变量。可以是值捕获 [x] 或引用捕获 [&x],也可以使用 [=] 捕获所有变量为值,[&] 捕获所有变量为引用。

  3. 参数列表:类似于普通函数的参数列表。

  4. 返回类型:可以显式指定返回类型,也可以自动推导。

  5. 函数体:定义 lambda 表达式的行为。

  6. mutable 关键字:允许在 lambda 表达式中修改值捕获的变量。

  7. 消除多重初始化路径

  8. 参数列表中[*this],是按值捕获自己对象,会创建一个新的对象;[this]是按引用捕获自己对象

Lambda 表达式在需要传递简单函数逻辑时非常有用,尤其是在 STL 算法中,它们可以使代码更简洁和易读。

三、泛型lambda表达式

泛型 lambda 表达式是 C++14 引入的一个特性,允许 lambda 表达式使用自动类型推导的参数类型,这使得 lambda 表达式更加灵活和通用。通过使用 auto 关键字,泛型 lambda 表达式可以处理多种类型的参数。

下面是一些泛型 lambda 表达式的示例:

示例 1: 简单的泛型 lambda 表达式

#include <iostream>

int main() {
    // 定义一个泛型 lambda 表达式,接受两个参数并返回它们的和
    auto add = [](auto a, auto b) {
        return a + b;
    };

    // 测试不同类型的加法
    std::cout << "3 + 4 = " << add(3, 4) << std::endl;    // 输出: 3 + 4 = 7
    std::cout << "3.5 + 4.5 = " << add(3.5, 4.5) << std::endl; // 输出: 3.5 + 4.5 = 8
    std::cout << "'a' + 'b' = " << add('a', 'b') << std::endl; // 输出: 'a' + 'b' = 195

    return 0;
}

示例 2: 泛型 lambda 表达式用于排序

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 9};

    // 使用泛型 lambda 表达式进行排序
    std::sort(numbers.begin(), numbers.end(), [](auto a, auto b) {
        return a < b;
    });

    // 输出排序后的结果
    for (int num : numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl; // 输出: 1 2 5 8 9

    return 0;
}

示例 3: 泛型 lambda 表达式用于打印任意类型的值

#include <iostream>
#include <vector>
#include <string>

int main() {
    // 定义一个泛型 lambda 表达式,用于打印任意类型的值
    auto print = [](const auto& value) {
        std::cout << value << std::endl;
    };

    // 测试不同类型的打印
    print(42);                // 输出: 42
    print(3.14);              // 输出: 3.14
    print("Hello, World!");   // 输出: Hello, World!

    std::vector<int> numbers = {1, 2, 3};
    print(numbers);           // 输出: 0x... (地址)

    return 0;
}

示例 4: 泛型 lambda 表达式用于处理自定义类型

#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    // 定义一个泛型 lambda 表达式,用于打印任意类型的成员
    auto printMember = [](const auto& obj, int (Point::*member)) {
        std::cout << obj.*member << std::endl;
    };

    Point p = {10, 20};

    // 打印 Point 对象的成员
    printMember(p, &Point::x); // 输出: 10
    printMember(p, &Point::y); // 输出: 20

    return 0;
}

总结

泛型 lambda 表达式通过使用 auto 关键字,使得 lambda 表达式可以接受和操作多种类型的参数。这种特性在需要处理多种类型数据时非常有用,可以增加代码的灵活性和可重用性。通过泛型 lambda 表达式,可以简化代码并减少重复的类型定义。

四、std::function

  • 不同类型的函数对象可以抹去所有差异,原型相同就可以放大function对象中
  • function对象创建和销毁可能使用堆上内存;执行则相当于普通虚函数调用

std::transform 是 C++ 标准库 <algorithm> 头文件中的一个算法函数,用于对容器(如 vectorliststring 等)中的元素进行转换操作,并将结果存储到另一个容器中。以下是对 std::transform 的详细介绍:

函数原型

std::transform 有两种重载形式:

  1. 单输入范围转换
template <class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first, UnaryOperation unary_op);

first1、last1:定义输入范围的迭代器。
d_first:输出范围的起始迭代器。
unary_op:对单个元素应用的一元操作函数或函数对象。
  1. 2.双输入范围转换
template <class InputIt1, class InputIt2, class OutputIt, class BinaryOperation>
OutputIt transform(InputIt1 first1, InputIt1 last1, InputIt2 first2, OutputIt d_first, BinaryOperation binary_op);

first1、last1:定义第一个输入范围的迭代器。
first2:定义第二个输入范围的起始迭代器。
d_first:输出范围的起始迭代器。
binary_op:对两个元素应用的二元操作函数或函数对象。

使用示例

  1. 单输入范围转换示例

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> squared_numbers(numbers.size());

    // 计算每个元素的平方
    std::transform(numbers.begin(), numbers.end(), squared_numbers.begin(),
        [](int x) { return x * x; });

    // 输出结果
    for (int num : squared_numbers) {
        std::cout << num << " ";
    }
    // 输出: 1 4 9 16 25

    return 0;
}

双输入范围转换示例

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers1 = {1, 2, 3, 4, 5};
    std::vector<int> numbers2 = {10, 20, 30, 40, 50};
    std::vector<int> results(numbers1.size());

    // 计算两个对应元素的和
    std::transform(numbers1.begin(), numbers1.end(), numbers2.begin(), results.begin(),
        std::plus<int>());

    // 输出结果
    for (int num : results) {
        std::cout << num << " ";
    }
    // 输出: 11 22 33 44 55

    return 0;
}

注意事项

  • 迭代器有效性:确保提供的迭代器是有效的,并且指向的元素数量匹配。
  • 输出容器大小:在调用 std::transform 之前,确保输出容器有足够的空间来存储结果。可以使用 resize 方法预先分配空间,或者使用 back_inserter 来动态添加元素。
  • Lambda 表达式:可以使用 lambda 表达式作为操作函数,以增加灵活性。
  • 性能std::transform 通常比手动循环更高效,因为编译器可能对其进行优化。

应用场景

  • 数值转换:如计算平方、加倍等。
  • 字符串处理:如转换大小写。
  • 数据合并:如计算两个向量的对应元素之和。
  • 类型转换:如将字符串转换为长度。

五、将若干个即将立即执行的 lambda 表达式放到 std::function 对象中,在某些场合下可能不太适合,尤其是当需要使用 function 模板进行类型擦除或存储时。这种做法可能带来以下问题:

1. 性能开销

  • 类型擦除成本std::function 是一个通用的函数包装器,它通过类型擦除(type erasure)来存储任意可调用对象。这种机制通常涉及动态内存分配和间接调用(通过虚函数或类似机制),导致性能开销。
  • 立即执行:如果 lambda 表达式是立即执行的,并且结果不需要被存储或复用,那么将其包装在 std::function 中是多余的,因为直接执行 lambda 表达式会更高效。

2. 代码复杂性和可读性

  • 不必要的间接性:将立即执行的 lambda 表达式放入 std::function 中,会使代码逻辑变得复杂和难以理解。读者可能会困惑为什么需要额外的包装层。
  • 冗余代码:如果 lambda 表达式只是为了立即执行某个操作,并且结果不需要进一步使用,那么直接执行它会更简洁明了。

3. 类型安全和灵活性

  • 类型不匹配std::function 需要指定签名(返回类型和参数类型)。如果 lambda 表达式的签名与 std::function 的模板参数不匹配,将导致编译错误。对于立即执行的 lambda 表达式,通常不需要这种类型约束。
  • 灵活性降低std::function 通常用于需要存储和延迟执行可调用对象的场景。如果 lambda 表达式是立即执行的,那么使用 std::function 反而限制了灵活性,因为无法利用编译时多态性。

4. 使用场景不匹配

  • 不适合立即执行std::function 更适合用于需要存储可调用对象并在稍后执行的场景,例如回调机制、事件处理等。对于立即执行的 lambda 表达式,这种存储和延迟执行的能力是多余的。
  • 替代方案:如果需要在某个作用域内执行多个操作,并且这些操作之间没有共享状态或延迟执行的需求,那么直接顺序执行这些 lambda 表达式会更合适。

示例说明

假设有以下场景,需要将多个立即执行的 lambda 表达式放入 std::function 中:

#include <iostream>
#include <functional>

int main() {
    // 不推荐的做法:将立即执行的 lambda 表达式放入 std::function 中
    std::function<void()> func1 = []() { std::cout << "Lambda 1 executed" << std::endl; };
    std::function<void()> func2 = []() { std::cout << "Lambda 2 executed" << std::endl; };

    func1(); // 立即执行
    func2(); // 立即执行

    // 更简单和高效的做法:直接执行 lambda 表达式
    []() { std::cout << "Direct Lambda 1 executed" << std::endl; }();
    []() { std::cout << "Direct Lambda 2 executed" << std::endl; }();

    return 0;
}

在这个例子中,直接执行 lambda 表达式比将它们包装在 std::function 中更简单、更高效。

结论

将若干个即将立即执行的 lambda 表达式放入 std::function 对象中,在大多数情况下是不合适的,因为:

  • 它引入了不必要的性能开销。
  • 它增加了代码的复杂性和降低了可读性。
  • 它限制了类型安全和灵活性。
  • 它与 std::function 的典型使用场景不匹配。

对于立即执行的 lambda 表达式,直接执行它们是更简单、更高效的选择。

六、推荐无差别地使用 [=](按值捕获所有外部变量)

在产品代码中,不推荐无差别地使用 [=](按值捕获所有外部变量)作为 lambda 表达式的默认捕获方式,主要原因涉及代码的可维护性、可读性、潜在的性能开销以及隐藏的逻辑错误风险。以下是详细的分析:


1. 降低代码的可读性和可维护性

  • 隐式依赖[=] 会捕获 lambda 表达式所在作用域内的所有外部变量,但不会显式列出这些变量。这使得代码的读者难以快速了解 lambda 表达式具体依赖哪些外部变量。
  • 难以调试:当 lambda 表达式出现意外行为时,由于捕获的变量不透明,调试和排查问题会变得更加困难。

示例对比

// 使用 [=] 捕获所有变量(不推荐)
auto func1 = [=]() {
    std::cout << x << ", " << y << std::endl;
};

// 显式捕获需要的变量(推荐)
auto func2 = [x, y]() {
    std::cout << x << ", " << y << std::endl;
};

在 func2 中,通过显式列出 x 和 y,代码的意图更加清晰。

2. 增加不必要的性能开销

  • 不必要的拷贝[=] 会对所有外部变量进行值拷贝,即使某些变量在 lambda 表达式中并未使用。这会导致不必要的内存分配和拷贝操作,影响性能。
  • 大对象的拷贝问题:如果捕获的变量是大型对象(如 std::vector 或自定义类),按值拷贝会显著增加开销。

示例

std::vector<int> largeData(1000000, 42);

// 不推荐:largeData 被按值拷贝,即使未使用
auto badLambda = [=]() {
    std::cout << "Hello" << std::endl;
};

// 推荐:按引用捕获或避免捕获
auto goodLambda = []() {
    std::cout << "Hello" << std::endl;
};

3. 可能导致悬垂引用或生命周期问题

  • 隐式按值捕获的潜在问题:虽然 [=] 是按值捕获,但如果 lambda 表达式被存储并在原变量生命周期结束后执行,可能引发未定义行为(尽管按值捕获本身不会导致悬垂引用,但混合使用 [=] 和 [&] 或捕获局部变量可能导致混淆)。
  • 更危险的是 [&]:如果误用 [&](按引用捕获所有变量),而 lambda 被延迟执行,则极易导致悬垂引用。虽然 [=] 本身不会,但混合使用会增加风险。

示例(潜在混淆)

int x = 10;
auto lambda = [=, &x]() {  // 混合使用 [=] 和 [&]
    x = 20;  // 修改的是外部的 x(引用捕获)
    std::cout << x << std::endl;
};
lambda();  // 输出 20
// 如果 lambda 被存储并在 x 生命周期结束后执行,则 [=] 捕获的其他变量可能安全,但这种混合使用容易出错

4. 难以追踪变量的修改

  • 修改的隐蔽性:如果 lambda 表达式修改了捕获的变量(通过引用捕获或某些隐式行为),使用 [=] 会让代码的读者难以发现这些修改,从而引发难以调试的错误。
  • 一致性缺失:团队成员可能对 [=] 的行为理解不一致,导致代码风格不统一。

示例

int value = 10;
auto func = [=]() mutable {  // 需要 mutable 才能修改值捕获的变量
    value = 20;  // 修改的是 lambda 内部的拷贝
};
func();
std::cout << value << std::endl;  // 输出 10(外部变量未被修改)
// 这种行为可能不符合预期,且难以通过代码直接发现

5. 推荐的最佳实践

  • 显式捕获:总是显式列出 lambda 表达式需要捕获的变量,避免使用 [=] 或 [&] 作为默认捕获方式。
    • 按值捕获:[x, y]
    • 按引用捕获:[&x, &y]
    • 混合捕获:根据需要组合使用,如 [x, &y]
  • 避免捕获不必要的变量:只捕获 lambda 表达式真正需要的变量。
  • 使用 mutable 谨慎:如果需要修改按值捕获的变量,明确使用 mutable 关键字,并确保这种修改是合理的。

总结

在产品代码中,不推荐使用 [=] 的主要原因是:

  1. 降低可读性和可维护性:隐式捕获所有变量,代码意图不清晰。
  2. 增加性能开销:不必要的拷贝,尤其是对于大对象。
  3. 潜在的生命周期和逻辑错误:混合使用捕获方式或对捕获行为理解不一致可能导致错误。
  4. 难以追踪修改:修改的隐蔽性增加了调试难度。

推荐做法:始终显式列出需要捕获的变量,确保代码清晰、高效且易于维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值