在 C++ 中,Callable Object(可调用对象) 指的是任何可以像函数一样被调用的事物。简单来说,就是任何可以使用 () 运算符(Function Call Operator)的对象。

在现代 C++(C++11 及以后)中,可调用对象的概念非常重要,因为它是 STL 算法、线程(std::thread)以及回调机制的核心。

C++ 中的可调用对象主要分为以下 5 类

1. 普通函数与函数指针 (Function Pointers)

这是最基础的形式,继承自 C 语言。

  • 特点: 没有状态(Stateless),行为固定。
  • 适用场景: 简单的回调,也就是所谓的 C 风格 API。
#include <print>

void hello() {
    std::println("Hello from Function Pointer!");
}

int main() {
    // 定义函数指针
    void (*funcPtr)() = &hello; // & 是可选的
    
    // 调用
    funcPtr(); 
    return 0;
}

Godbolt

2. 仿函数 (Functors / Function Objects)

仿函数是 C++ 面向对象特性的体现。它是重载了 operator() 的类对象

  • 特点:

  • 它可以拥有状态 (State):这是它比函数指针强大的核心原因。你可以通过成员变量保存数据。

  • 内联优化:编译器很容易将仿函数的调用内联(Inline),通常比函数指针更快。

  • 适用场景: 需要在多次调用之间保持状态,或者需要高度优化的 STL 算法(如 std::sort)。

#include <iostream>

class Adder {
    int distinct_val; // 内部状态
public:
    Adder(int v) : distinct_val(v) {}

    // 重载 () 运算符
    int operator()(int x) const {
        return x + distinct_val;
    }
};

int main() {
    Adder add10(10); // 创建一个“加10”的加法器
    std::cout << add10(5) << std::endl; // 输出 15
    return 0;
}

3. Lambda 表达式 (Lambda Expressions)

引入于 C++11,Lambda 是现代 C++ 最常用的可调用对象。

  • 本质: Lambda 实际上是匿名仿函数的语法糖。编译器在幕后为你生成了一个重载了 operator() 的类。
  • 特点: 代码紧凑,可以直接在调用点定义。
  • 捕获列表 [] 允许你捕获上下文中的变量(按值或按引用),这对应仿函数的成员变量。
#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    int factor = 2;
    std::vector<int> nums = {1, 2, 3};

    // [factor] 是捕获列表,捕获外部变量
    std::for_each(nums.begin(), nums.end(), [factor](int n) {
        std::cout << n * factor << " ";
    });
    // 输出: 2 4 6
    return 0;
}

4. std::function (通用多态包装器)

std::function 是 C++11 引入的一个标准库模板类,位于 <functional> 头文件。

  • 特点: 它是一个类型擦除(Type Erasure)的容器。它可以存储任何符合特定签名的可调用对象(函数指针、仿函数、Lambda 等)。
  • 代价: 由于使用了虚函数机制和可能的堆内存分配,它比直接使用 Lambda 或模板有轻微的性能开销。
  • 适用场景: 当你需要存储不同类型的回调,或者作为函数参数不需要模板化时。
#include <iostream>
#include <functional>

int add(int a, int b) { return a + b; }

int main() {
    // 1. 包装普通函数
    std::function<int(int, int)> func = add;
    
    // 2. 包装 Lambda
    func = [](int a, int b) { return a * b; };
    
    // 3. 包装仿函数
    struct Divisor { int operator()(int a, int b) { return a / b; } };
    func = Divisor();

    std::cout << func(10, 2) << std::endl; // 调用的是最后赋值的 Divisor,输出 5
    return 0;
}

5. 类的成员函数指针 (Pointers to Member Functions)

这是一个比较特殊且语法晦涩的类别。非静态成员函数需要依赖一个对象实例才能调用。

  • 难点: 不能直接像 f() 那样调用,通常需要 (obj.*ptr)(args)(objptr->*ptr)(args)
  • 现代解法: 使用 std::mem_fnstd::bind(虽已过时),或者在 C++17 中使用 std::invoke
#include <iostream>
#include <functional>

class Foo {
public:
    void print(int x) { std::cout << "Foo: " << x << std::endl; }
};

int main() {
    Foo obj;
    // 定义成员函数指针
    void (Foo::*ptr)(int) = &Foo::print;

    // 传统调用方式 (非常丑陋)
    (obj.*ptr)(42);

    // 现代方式:使用 std::mem_fn
    auto runnable = std::mem_fn(&Foo::print);
    runnable(obj, 42); // 第一个参数必须是对象实例
    return 0;
}

6. 总结与对比

为了让你更直观地理解,我做了一个对比表:

类型是否有状态灵活性性能典型用途
函数指针高 (但在内联方面不如仿函数)C 接口兼容,简单的全局回调
仿函数极高 (易被编译器内联)需要状态的复杂逻辑,STL 算法
Lambda (通过捕获)极高 (同仿函数)绝大多数现代 C++ 场景,局部逻辑
std::function极高 (可存任何类型)中 (虚函数开销,堆分配)API 接口设计,存储异构回调列表
成员函数指针依赖对象特定类操作,通常配合 bind/mem_fn 使用

7. std::invoke (C++17)

由于上面提到的调用方式五花八门(有的直接用 (),有的要用 .*),C++17 引入了 std::invoke统一所有可调用对象的调用语法。

统一调用语法

std::invoke 的强大之处在于它抹平了普通函数、Lambda、成员函数甚至成员变量之间的调用差异。

#include <iostream>
#include <functional> // std::invoke 所在头文件

struct MyStruct {
  int value{42};
  void printSum(int n) const {
    std::println("Sum: {}", value + n);
  }
};

void plainFunction(int n) {
  std::println("Plain: {}", n);
}

int main() {
  MyStruct obj;

  // 1. 调用普通函数
  std::invoke(plainFunction, 10);

  // 2. 调用 Lambda
  auto lambda = [](int n) { std::println("Lambda: {}", n); };
  std::invoke(lambda, 20);

  // 3. 调用成员函数 (第一个参数是对象引用或指针)
  std::invoke(&MyStruct::printSum, obj, 30);

  // 4. 访问成员变量 (没错,成员变量也被视为“可调用”的,返回其值)
  std::println("Member value: {}", std::invoke(&MyStruct::value, obj));

  return 0;
}

为什么需要它?(泛型编程的神器)

如果你在编写一个模板函数,需要接受一个“可调用对象”并执行它,在没有 std::invoke 之前,你很难处理成员函数指针。

坏品味的代码 (C++11 以前):

template <typename F, typename... Args>
void callIt(F f, Args&&... args) {
  // 如果 f 是成员函数指针,这里会编译报错!
  // 你必须写一堆 std::enable_if 或重载来区分情况
  f(std::forward<Args>(args)...); 
}

好品味的代码 (使用 std::invoke):

template <typename F, typename... Args>
auto callIt(F&& f, Args&&... args) {
  // 无论 f 是什么,一把梭,全部搞定
  return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}

在现代 C++ 的库开发中,std::invoke 是实现高阶函数(如 std::thread 的构造函数、std::async 等)的基石。它让代码更简洁,消除了不必要的特殊情况处理。

8. std::apply (C++17)

如果说 std::invoke 解决了"如何统一调用"的问题,那么 std::apply 解决的就是"如何将 tuple 展开为参数"的问题。

核心功能

std::apply 接受一个可调用对象和一个 tuple,将 tuple 中的元素解包后作为参数传递给可调用对象。这本质上就是 std::invoke + tuple 解包的组合。

#include <iostream>
#include <functional>
#include <tuple>

int add(int a, int b, int c) {
  return a + b + c;
}

int main() {
  // 将 tuple 中的 1, 2, 3 解包后传给 add
  auto args = std::make_tuple(1, 2, 3);
  int result = std::apply(add, args);
  std::println("Result: {}", result); // 输出 6

  // 配合 Lambda 使用
  auto lambda = [](int x, int y) { return x * y; };
  auto pair = std::make_pair(3, 4);
  std::println("Lambda result: {}", std::apply(lambda, pair)); // 输出 12

  return 0;
}

为什么需要它?

在没有 std::apply 之前,如果你想调用一个函数,但参数被包装在 tuple 里,你必须手动解包:

坏品味的代码 (手动解包):

template <typename F, typename Tuple>
auto callWithTuple(F f, const Tuple& t) {
  // 假设 tuple 有 3 个元素,你必须这样写:
  return f(std::get<0>(t), std::get<1>(t), std::get<2>(t));
  // 如果 tuple 大小不确定?完蛋,模板元编程地狱等着你
}

好品味的代码 (使用 std::apply):

template <typename F, typename Tuple>
auto callWithTuple(F&& f, Tuple&& t) {
  return std::apply(std::forward<F>(f), std::forward<Tuple>(t));
}

实际应用场景

std::apply 在处理动态参数列表时特别有用,比如:

#include <iostream>
#include <tuple>
#include <vector>
#include <numeric>

// 计算任意数量参数的和
auto sum_all = [](auto... args) {
  return (args + ...); // C++17 折叠表达式
};

int main() {
  // 从 vector 构造 tuple(需要编译期确定大小)
  // 这里演示静态 tuple 的应用
  auto values = std::make_tuple(1, 2, 3, 4, 5);
  std::println("Sum: {}", std::apply(sum_all, values)); // 输出 15

  // 配合成员函数使用
  struct Calculator {
    int multiply(int a, int b, int c) { return a * b * c; }
  };
  
  Calculator calc;
  auto member_args = std::make_tuple(&calc, 2, 3, 4);
  std::println("Product: {}", std::apply(&Calculator::multiply, member_args)); // 输出 24

  return 0;
}

std::invoke vs std::apply

特性std::invokestd::apply
参数传递方式直接传递参数从 tuple 解包传递
适用场景参数已知,需要统一调用语法参数被打包在 tuple 中
底层机制统一调用封装std::invoke + tuple 解包

简单来说:std::apply(f, args) 等价于 std::invoke(f, std::get<0>(args), std::get<1>(args), ...)