1. 项目概述:C++11新特性的实战价值

如果你已经用C++写过一些项目,从简单的控制台程序到稍复杂的网络应用,你可能会在某个深夜对着代码陷入沉思:为什么实现一个通用的日志函数要写那么多重载版本?为什么为了一个简单的比较逻辑,要专门去定义一个结构体并重载 operator() ?又或者,为什么总感觉标准库里的某些组件用起来有点“隔靴搔痒”,不够顺手?这些问题,在C++11之前,几乎是每个C++开发者成长路上的必修课,我们不得不与繁琐的模板元编程、冗长的仿函数定义以及各种为了通用性而妥协的设计模式作斗争。

C++11标准的发布,就像给这门古老而强大的语言注入了一剂强心针。它带来的不仅仅是一些语法糖,更是一整套提升开发效率、增强代码表达力、并从根本上改变我们编写C++代码思维方式的新工具。今天我们要深入探讨的,正是其中三个极具代表性的特性: 可变参数模板 lambda表达式 包装器 。它们分别从 泛型编程的终极形态 函数式编程的轻量级入口 以及 可调用对象的统一抽象 三个维度,重塑了现代C++的面貌。

简单来说,掌握了这三个特性,你就能:

  • 告别笨重的重载 :用可变参数模板写出真正“一劳永逸”的通用函数和类,例如一个能处理任意数量、任意类型参数的完美转发日志器。
  • 就地定义匿名逻辑 :用lambda表达式在需要的地方直接编写短小的函数对象,让排序、查找、线程任务等代码变得异常简洁直观。
  • 统一管理各种“可调用体” :用 std::function 等包装器,将函数指针、成员函数指针、lambda、仿函数等不同“长相”但相同“内涵”(都是可调用)的对象,放到同一个篮子里管理和传递,极大地提升了接口的灵活性。

这不仅仅是语法学习,更是一次编程范式的升级。接下来,我将结合大量实际编码场景和踩坑经验,带你彻底吃透这三个特性,让你写的C++代码从此告别“上古时代”,焕发现代化的简洁与力量。

2. 可变参数模板:泛型编程的“终极武器”

在C++98/03时代,模板虽然强大,但其参数数量是固定的。如果你想写一个能打印任意数量参数的函数,可能需要写出 print(T1) print(T1, T2) , 甚至 print(T1, T2, T3) 等多个重载版本,这显然违背了DRY(Don‘t Repeat Yourself)原则。可变参数模板的出现,彻底解决了这个问题。

2.1 核心语法与递归展开模式

可变参数模板的语法核心是 typename... Args class... Args ,这里的 ... 就是一个“参数包”,它可以包含0个或多个模板参数。

// 声明一个可变参数模板函数
template<typename T, typename... Args>
void print(const T& first, const Args&... rest) {
    std::cout << first << " ";
    print(rest...); // 递归调用,展开参数包
}

// 递归终止条件:当参数包为空时调用此版本
void print() {
    std::cout << std::endl;
}

为什么需要递归终止条件? 这是理解可变参数模板运作的关键。函数模板 print 在每次调用时,将第一个参数 first 处理掉,然后将剩余的 rest 参数包继续传递给 print 。这个过程递归进行,直到参数包 rest 为空。此时,编译器需要找到一个参数列表为空的 print 函数来匹配调用,这就是我们提供的无参版本 print() ,它结束了递归并输出换行。

注意 :这个递归终止函数 必须 在递归展开版本之前声明或定义,否则编译器在尝试匹配空参数包时会找不到合适的函数,导致编译错误。这是新手最容易踩的坑之一。

2.2 更现代的展开方式:折叠表达式(C++17)

递归展开虽然经典,但写起来略显繁琐,且可能影响编译速度。C++17引入了 折叠表达式 ,让参数包的展开变得异常优雅和高效。

// 使用折叠表达式实现print,无需递归终止函数
template<typename... Args>
void print(Args&&... args) {
    // 二元左折叠: ((std::cout << args) << ...)
    // 展开为:std::cout << arg1 << arg2 << arg3 ...
    (std::cout << ... << args) << std::endl;
}

// 计算所有参数的和
template<typename... Args>
auto sum(Args... args) {
    // 一元右折叠: (args + ...)
    // 展开为:arg1 + (arg2 + (arg3 + ...))
    return (args + ...);
}

折叠表达式的优势

  1. 代码极其简洁 :一行代码搞定,无需辅助函数或递归终止。
  2. 编译期求值 :对于常量表达式参数,结果可以在编译期计算,零运行时开销。
  3. 支持多种运算符 :不仅限于 << + ,几乎所有二元运算符都支持( + , - , * , / , % , ^ , & , | , << , >> , += , -= , *= , /= , %= , ^= , &= , |= , <<= , >>= ),以及逗号运算符 , 和逻辑运算符 && ||

实操心得 :在现代C++项目(要求C++17及以上)中, 应优先使用折叠表达式来展开参数包 。它不仅是语法糖,更是一种更符合直觉、更高效的范式。只有在你需要更复杂的、非线性的处理逻辑时(例如需要根据参数类型进行不同的分支处理),才考虑使用递归展开。

2.3 实战应用:实现一个“完美转发”的工厂函数

可变参数模板结合 完美转发 ,可以构建出类型安全且高效的通用工厂函数,这是现代C++库设计的基石之一。

#include <memory>
#include <utility>

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    // 使用 std::forward 保持参数的左值/右值引用属性
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

class Widget {
public:
    Widget(int a, const std::string& b, double c) {
        std::cout << "Widget constructed with: " << a << ", " << b << ", " << c << std::endl;
    }
};

int main() {
    // 完美转发所有构造参数
    auto w1 = make_unique<Widget>(42, "hello", 3.14);
    // 同样可以转发临时对象(右值)
    auto w2 = make_unique<Widget>(100, std::string("world"), 2.71);
    return 0;
}

关键点解析

  • Args&&... args :这是一个 转发引用 (又称万能引用),它能同时匹配左值和右值。
  • std::forward<Args>(args)... :这是在展开参数包的同时,对每个参数应用 std::forward 。这确保了如果传入的是一个右值临时对象,它将以右值引用的形式传递给 T 的构造函数,从而可能触发移动语义,提升性能;如果传入的是一个左值,则保持左值引用属性。
  • new T(std::forward<Args>(args)...) :这里的 ... 展开发生在函数参数列表内部,是可变参数模板的另一种典型用法。

避坑指南 :自己实现类似 make_unique 的函数时,要特别注意异常安全。标准库的实现通常使用 new 运算符,并保证在构造失败时不会内存泄漏。在更复杂的场景下,你可能需要考虑使用 try-catch 块或 std::uninitialized_construct 等底层工具来保证强异常安全。

3. Lambda表达式:匿名函数的艺术

在C++11之前,如果你想在算法中传入一个自定义的比较或操作逻辑,通常有两种选择:定义一个独立的函数(如果逻辑简单,会污染命名空间),或者定义一个仿函数类(代码冗长)。Lambda表达式提供了一种在调用点就地定义匿名函数对象的优雅方式。

3.1 语法全解与捕获列表详解

Lambda表达式的基本语法是: [捕获列表] (参数列表) -> 返回类型 { 函数体 } 。其中,参数列表、返回类型在某些情况下可以省略。

std::vector<int> nums = {1, 5, 3, 4, 2};

// 1. 最简单的lambda:排序(省略返回类型,编译器推导为bool)
std::sort(nums.begin(), nums.end(), [](int a, int b) { return a < b; });

// 2. 指定返回类型
auto add = [](double a, double b) -> int {
    return static_cast<int>(a + b); // 显式返回int
};

// 3. 无参数的lambda
auto greet = [] { std::cout << "Hello, Lambda!" << std::endl; };

捕获列表是Lambda的灵魂 ,它决定了Lambda函数体内部如何访问外部变量。

  • [] :不捕获任何外部变量。
  • [=] :以 的方式捕获所有外部变量(在Lambda定义时拷贝一份)。
  • [&] :以 引用 的方式捕获所有外部变量。
  • [var] :以值的方式捕获特定变量 var
  • [&var] :以引用的方式捕获特定变量 var
  • [=, &var] :默认以值捕获,但变量 var 以引用捕获。
  • [&, var] :默认以引用捕获,但变量 var 以值捕获。
  • [this] :捕获当前类的 this 指针,从而可以访问类成员。

重要经验:慎用默认捕获 [=] [&] 默认捕获虽然方便,但极易引入隐蔽的bug。

  • [&] 的风险:如果Lambda的生命周期超过了它所捕获的引用的变量的生命周期(例如,将Lambda存入一个容器以备后续调用,而捕获的局部变量已经销毁),就会导致 悬垂引用 ,引发未定义行为。
  • [=] 的陷阱:在C++11中, [=] 捕获的是 Lambda定义时 变量的值。如果后续变量值改变,Lambda内部看到的仍然是旧值。更关键的是,它可能让你误以为捕获的是“当前值”,而实际上是一个固定的拷贝。

最佳实践 显式列出所有需要捕获的变量 ,并仔细选择值捕获( var )还是引用捕获( &var )。这迫使你思考每个变量的生命周期和用途,代码也更清晰、更安全。

3.2 广义捕获(C++14)与移动语义

C++14引入了 初始化捕获 (或称广义捕获),允许你在捕获列表中直接初始化变量,这为实现移动捕获打开了大门。

std::unique_ptr<Resource> heavyResource = std::make_unique<Resource>();

// C++11无法直接移动捕获unique_ptr(因为它不可拷贝)
// C++14可以通过初始化捕获实现
auto task = [resource = std::move(heavyResource)] {
    // 现在`resource`是Lambda对象的一部分,通过移动语义获得所有权
    resource->doWork();
};
// 此时heavyResource变为nullptr

// 同样可以用于值捕获并修改
int x = 10;
auto lambda = [y = x + 5] { return y; }; // y被初始化为15

为什么这很重要? 在异步编程、线程池等场景中,我们经常需要将资源和任务一起打包传递。 std::unique_ptr std::future 等只移动类型无法用传统的 [=] [&] 捕获。广义捕获使得Lambda可以安全、高效地“拥有”这些资源,是现代C++并发编程的必备技巧。

3.3 mutable关键字与闭包类型

默认情况下,Lambda的 operator() 是一个 const 成员函数。这意味着,对于以值方式捕获的变量,你无法在Lambda体内修改它的拷贝。

int count = 0;
// 错误:无法修改值捕获的变量
// auto f = [count] { count++; };

// 正确:使用mutable关键字
auto f = [count]() mutable {
    count++;
    std::cout << "Internal count: " << count << std::endl;
};
f(); // 输出:Internal count: 1
f(); // 输出:Internal count: 2
std::cout << "External count: " << count << std::endl; // 输出:External count: 0

关键理解

  1. mutable 移除的是函数调用运算符的 const 属性, 而不是 允许你修改外部原始变量。上面例子中,外部 count 始终是0,修改的是Lambda内部自己的那份拷贝。
  2. 一旦使用了 mutable ,参数列表 () 就不能省略,即使它为空。
  3. 每个Lambda表达式都会生成一个独一无二的、匿名的 闭包类型 auto f = ... 中的 f 就是这个闭包类型的一个对象。你可以把它理解为一个编译器自动生成的、重载了 operator() 的类。

4. 包装器:统一可调用对象的“万能胶”

C++中有多种可调用对象:普通函数、函数指针、成员函数指针、仿函数对象、Lambda表达式。它们的类型各不相同,给泛型编程和回调机制带来了麻烦。 std::function 就是一个通用的、类型擦除的 可调用对象包装器 ,它提供了一种统一的方式来保存、复制和调用任何满足签名要求的可调用实体。

4.1 std::function 的基本用法

std::function 的模板参数是一个函数签名,例如 std::function<int(int, int)> 表示一个接受两个 int 参数并返回 int 的可调用对象。

#include <functional>
#include <iostream>
#include <vector>

int add(int a, int b) { return a + b; }
struct Multiply {
    int operator()(int a, int b) const { return a * b; }
};

int main() {
    std::function<int(int, int)> func;

    // 包装普通函数
    func = add;
    std::cout << func(2, 3) << std::endl; // 输出 5

    // 包装仿函数对象
    func = Multiply();
    std::cout << func(2, 3) << std::endl; // 输出 6

    // 包装lambda表达式
    func = [](int a, int b) { return a - b; };
    std::cout << func(5, 3) << std::endl; // 输出 2

    // 放入容器,实现回调机制
    std::vector<std::function<int(int, int)>> operations = {add, Multiply(), [](int a, int b){return a/b;}};
    for (auto& op : operations) {
        std::cout << op(10, 2) << " "; // 输出 5 20 5
    }
    std::cout << std::endl;

    return 0;
}

4.2 实现原理浅析与性能考量

std::function 通常使用一种称为“小对象优化”的技术。它内部有一个小的缓冲区(例如16或32字节),如果存储的可调用对象(如小的无捕获Lambda、函数指针)尺寸小于这个缓冲区,就将其直接存储在 std::function 对象内部(栈上),避免堆内存分配。如果对象较大(如捕获了大量变量的Lambda),则会在堆上分配内存。

性能提示

  • 构造和复制开销 std::function 的构造、复制和移动操作可能涉及内存分配和类型擦除,有一定开销。在性能极度敏感的循环内部,应避免频繁创建或复制 std::function
  • 与模板参数对比 :如果是在泛型代码中(如模板函数),直接使用模板参数(如 template<typename F> void foo(F&& f) )通常比使用 std::function 更高效,因为前者可以进行内联优化,而后者多了一层间接调用。 std::function 的优势在于 类型擦除 带来的运行时多态和存储灵活性。
  • 空状态检查 std::function 可以处于空状态(未包装任何可调用对象)。在调用前,应使用 if(func) func.target_type() == typeid(void) 进行检查,否则调用空 std::function 会抛出 std::bad_function_call 异常。

4.3 std::bind 与占位符

std::bind 用于生成一个新的可调用对象,它通过“绑定”部分参数,将某个可调用对象适配成签名不同的另一个可调用对象。它常与 std::function 配合使用。

#include <functional>
using namespace std::placeholders; // 引入 _1, _2, ...

void printSum(int a, int b, const std::string& msg) {
    std::cout << msg << ": " << (a + b) << std::endl;
}

class Printer {
public:
    void print(const std::string& s) const {
        std::cout << "Printer: " << s << std::endl;
    }
};

int main() {
    // 1. 绑定普通函数,固定第一个和第三个参数
    auto boundFunc = std::bind(printSum, 10, _1, "The result is");
    boundFunc(20); // 等价于 printSum(10, 20, "The result is");

    // 2. 绑定成员函数,需要传入对象指针或引用
    Printer pr;
    auto memberFunc = std::bind(&Printer::print, &pr, _1);
    memberFunc("Hello"); // 等价于 pr.print("Hello");

    // 3. 绑定lambda,调整参数顺序
    auto sub = [](int a, int b){ return a - b; };
    auto reversedSub = std::bind(sub, _2, _1); // 参数顺序交换
    std::cout << reversedSub(5, 3) << std::endl; // 输出 2 (计算3-5)

    return 0;
}

std::bind 的注意事项

  1. 占位符 _1 , _2 , _3 ... 分别代表新生成可调用对象的第一、第二、第三个参数,它们定义在 std::placeholders 命名空间中。
  2. 参数传递方式 std::bind 默认按值绑定参数。如果你需要按引用绑定,必须使用 std::ref std::cref 进行包装,例如 std::bind(f, std::ref(a), _1)
  3. 与Lambda的对比 :在现代C++中, Lambda表达式几乎总是比 std::bind 更好的选择 。Lambda语法更清晰、更直观,通常也能产生更高效的代码,并且对捕获变量的控制更精细。 std::bind 在某些需要复杂参数重排或兼容旧代码的场景下仍有其价值,但新代码应优先考虑Lambda。

5. 三大特性联合实战:构建一个简单的事件系统

让我们用一个综合案例,将可变参数模板、Lambda和 std::function 结合起来,实现一个简单的事件发布-订阅系统。这个系统允许订阅者注册回调函数(Lambda、函数指针等),当事件发布时,所有回调都会被调用。

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

// 事件发射器类模板,EventType是事件的类型标识(如枚举或字符串)
template<typename EventType>
class EventEmitter {
private:
    // 使用std::function包装回调,回调可以接受任意参数
    using Callback = std::function<void()>; // 注意:这里先定义为无参,实际参数通过Lambda捕获或bind绑定

    // 存储事件的回调列表
    std::vector<Callback> callbacks_;

public:
    // 订阅事件:接受任何可调用对象,用可变参数模板和完美转发绑定参数
    template<typename... Args>
    void on(EventType event, void (*func)(Args...), Args&&... args) {
        // 使用Lambda捕获函数指针和绑定的参数
        callbacks_.push_back([=]() mutable {
            func(args...);
        });
    }

    // 更通用的订阅版本:接受任何std::function
    void subscribe(EventType event, Callback cb) {
        callbacks_.push_back(std::move(cb));
    }

    // 发布事件:触发所有回调
    void emit(EventType event) {
        for (auto& cb : callbacks_) {
            if (cb) {
                cb(); // 调用回调
            }
        }
    }
};

// 示例用法
enum class Event { Click, Hover, Close };

void globalClickHandler(int x, int y, const std::string& button) {
    std::cout << "Global Click at (" << x << "," << y << ") with " << button << " button." << std::endl;
}

int main() {
    EventEmitter<Event> emitter;

    // 订阅方式1:使用on模板方法绑定全局函数和参数
    emitter.on(Event::Click, globalClickHandler, 100, 200, "left");

    // 订阅方式2:使用subscribe方法直接传入Lambda
    emitter.subscribe(Event::Hover, []() {
        std::cout << "Hover event triggered!" << std::endl;
    });

    // 发布事件
    std::cout << "--- Emitting Click ---" << std::endl;
    emitter.emit(Event::Click);

    std::cout << "\n--- Emitting Hover ---" << std::endl;
    emitter.emit(Event::Hover);

    return 0;
}

这个案例的精妙之处

  1. 可变参数模板 on 方法利用可变参数模板 Args... 和完美转发 Args&&... args ,可以接受与目标函数签名匹配的任意数量和类型的参数,并将其“冻结”在Lambda的捕获列表中。
  2. Lambda表达式 :它作为“胶水”,将函数指针 func 和绑定的参数 args... 打包成一个无参的 Callback std::function<void()> ),从而统一了存储和调用接口。
  3. std::function包装器 std::function<void()> 作为统一的容器,可以存储由不同方式生成(函数指针+绑定、纯Lambda等)但最终签名一致的可调用对象。

扩展思考 :一个更健壮的事件系统还需要考虑线程安全、回调的优先级、一次性回调、取消订阅等功能。你可以尝试在此基础上,使用 std::mutex 保护 callbacks_ ,为每个 EventType 维护独立的回调列表,或者为每个订阅返回一个 token 用于后续取消订阅。

6. 常见问题与排查技巧实录

在实际项目中应用这些特性时,你肯定会遇到一些编译错误或运行时陷阱。下面是我总结的一些典型问题和解决方法。

6.1 可变参数模板相关

问题1:递归展开时遇到“no matching function”编译错误。

  • 现象 :编译错误指向递归调用的那一行,提示找不到合适的重载函数。
  • 原因 :最可能的原因是缺少递归终止函数,或者终止函数的签名与递归展开版本在空参数包情况下无法匹配。
  • 解决
    • 确保提供了无参的终止函数。
    • 检查终止函数是否在递归模板函数之前声明或定义。
    • 如果使用折叠表达式,则不需要终止函数,检查折叠表达式语法是否正确。

问题2:使用 std::forward 时,参数类型似乎不对。

  • 现象 :模板函数内部,参数似乎失去了引用属性,或者移动语义没有生效。
  • 原因 std::forward 的使用有误。 std::forward 必须用在模板函数中,且模板参数类型必须是 T&& 这种转发引用形式。
  • 正确写法
    template<typename... Args>
    void foo(Args&&... args) { // Args&& 是转发引用
        bar(std::forward<Args>(args)...); // 正确
    }
    
    template<typename... Args>
    void foo(Args... args) { // 这里是值传递
        bar(std::forward<Args>(args)...); // 错误!args不是转发引用
    }
    

6.2 Lambda表达式相关

问题1:Lambda捕获了局部变量的引用,但后续调用时程序崩溃。

  • 现象 :程序随机崩溃,调试发现崩溃点在Lambda内部,访问的指针或引用无效。
  • 原因 悬垂引用 。Lambda捕获了局部变量的引用,但该局部变量已经离开了其作用域(例如函数返回),Lambda被调用时访问了已销毁的内存。
  • 解决
    • 首选 :如果Lambda需要延长变量的生命周期,使用 值捕获 移动捕获 (C++14)。
    • 确保生命周期 :如果必须使用引用捕获,你必须百分百确定被引用的对象在Lambda被调用时依然有效。例如,捕获类成员变量(通过 [this] [&] )时,要确保类对象本身未被销毁。
    • 使用智能指针 :对于动态分配的对象,考虑用 std::shared_ptr 捕获,共享所有权。

问题2:在Lambda内修改值捕获的变量,但编译报错。

  • 现象 :错误信息类似“ operator() is a const member function`”。
  • 原因 :非 mutable 的Lambda,其 operator() const 的,不允许修改值捕获的变量(这些变量成为闭包对象的 const 成员)。
  • 解决 :在Lambda参数列表后添加 mutable 关键字。但请记住,这修改的是Lambda内部拷贝的值,不影响外部原始变量。

6.3 std::function 与 std::bind 相关

问题1:调用空的 std::function 导致程序异常终止。

  • 现象 :抛出 std::bad_function_call 异常。
  • 原因 std::function 对象未包装任何可调用目标就被调用了。
  • 解决 :在调用前总是检查其是否为空。
    std::function<void()> task;
    // ... task可能被赋值,也可能没有
    if (task) { // 或者 if (task != nullptr)
        task();
    } else {
        // 处理空回调的情况
    }
    

问题2:使用 std::bind 绑定成员函数时,对象生命周期管理不当。

  • 现象 :绑定的对象已被删除,但回调仍在被调用,导致访问违规。
  • 原因 std::bind(&Class::method, &object, ...) 捕获的是对象指针。如果 object 是局部变量且已销毁,或者被手动 delete ,指针便悬垂了。
  • 解决
    • 如果对象生命周期由你控制,确保回调的生命周期不超过对象。
    • 考虑使用 std::shared_ptr std::weak_ptr 来管理对象。
      auto objPtr = std::make_shared<MyClass>();
      auto callback = std::bind(&MyClass::method, objPtr, _1); // 共享所有权,安全
      
    • 在许多现代场景下,使用Lambda捕获 this (或智能指针)是更清晰的选择。

7. 性能优化与进阶技巧

当你熟练使用这些特性后,可以进一步关注如何让代码更高效、更优雅。

1. 对Lambda使用 constexpr (C++17) 如果Lambda只依赖于编译期可知的参数,可以将其声明为 constexpr ,这样它可以在编译期求值,用于模板元编程等场景。

constexpr auto square = [](int n) { return n * n; };
static_assert(square(5) == 25); // 编译期计算

2. 使用 auto 参数的泛型Lambda(C++14) Lambda的参数类型可以用 auto 声明,这实际上创造了一个模板化的 operator() ,使得Lambda可以接受任何类型的参数,更加通用。

auto genericAdder = [](auto a, auto b) { return a + b; };
std::cout << genericAdder(1, 2) << std::endl; // 3
std::cout << genericAdder(1.5, 2.3) << std::endl; // 3.8
std::string s1 = "Hello, ", s2 = "World!";
std::cout << genericAdder(s1, s2) << std::endl; // Hello, World!

3. 将Lambda作为返回值(高阶函数) 函数可以返回一个Lambda,这可以用来创建配置好的函数对象,是实现函数式编程中“柯里化”和“闭包”的重要手段。

auto makeMultiplier(int factor) {
    // 返回一个捕获了factor的Lambda
    return [factor](int value) { return value * factor; };
}
auto doubleIt = makeMultiplier(2);
auto tripleIt = makeMultiplier(3);
std::cout << doubleIt(5) << std::endl; // 10
std::cout << tripleIt(5) << std::endl; // 15

4. 在模板中优先使用泛型参数而非 std::function 如前所述,在模板函数或类中,如果可调用对象的类型是模板参数,直接使用该类型通常比 std::function 更高效。

// 高效版本:可内联
template<typename Func>
void processData(Func&& f) {
    // ... 一些操作
    f();
    // ... 更多操作
}

// 灵活但可能有开销的版本:类型擦除
void processData(std::function<void()> f) {
    // ... 一些操作
    f();
    // ... 更多操作
}

在性能关键的路径上,选择第一个版本;当需要存储或传递不同类型但签名相同的可调用对象时,选择第二个版本。

掌握C++11的这三个特性——可变参数模板、Lambda表达式和包装器,就如同为你的C++工具箱添置了三件神兵利器。它们分别从泛型、函数式和对象抽象三个层面,极大地提升了代码的表达力、简洁性和灵活性。从今天起,尝试在你的新代码中摒弃旧的模式,多用Lambda替代手写仿函数,用可变参数模板构建通用组件,用 std::function 设计清晰的回调接口。最初的适应可能需要一点时间,但一旦习惯,你会发现你写出的代码将更加现代、高效和易于维护。

更多推荐