当然可以!std::invoke 和 std::variant 是 C++17 中两个非常实用但用途迥异的标准库组件:

  • std::invoke

    统一调用机制,解决“如何以一致方式调用各种可调用对象”的问题。

  • std::variant

    类型安全的联合体(tagged union),用于在多个类型中安全地存储一个值。

下面分别对二者进行深入使用总结,包含核心原理、典型场景、代码示例与最佳实践。


一、std::invoke —— 统一调用一切可调用对象

✅ 核心作用

提供一个通用接口,可以统一调用以下任意形式的“可调用物”:

  • 普通函数

  • 函数指针

  • Lambda 表达式

  • 函数对象(Functor)

  • 成员函数指针
  • 成员变量指针

💡 在 C++17 之前,要写一个能同时调用 f() 和 obj.mem_fn() 的泛型代码非常麻烦。std::invoke 解决了这个问题。

🔧 基本语法

#include <functional>

std::invoke(f, args...);

📌 典型使用场景与示例

1. 调用普通函数 / Lambda / Functor

void foo(int x) { std::cout << x; }

auto lambda = [](int x) { return x * 2; };

struct Adder {

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

};

std::invoke(foo, 42);           // 调用 foo(42)

std::invoke(lambda, 5);         // 返回 10

std::invoke(Adder{}, 3, 4);     // 返回 7

2. 调用成员函数(关键优势!)

struct Person {

    void greet() const { std::cout << "Hello\n"; }

    int age = 25;

};

Person p;

// 传统方式: (p.*&Person::greet)()

// 使用 invoke:

std::invoke(&Person::greet, p);        // 对象

std::invoke(&Person::greet, &p);       // 指针(自动解引用)

// 带参数的成员函数

struct Math {

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

};

Math m;

int result = std::invoke(&Math::add, m, 10, 20); // 30

3. 访问成员变量(常被忽略但很有用)

std::invoke(&Person::age, p);   // 返回 25(等价于 p.age)

std::invoke(&Person::age, &p);  // 同样返回 25

4. 在泛型代码中使用(如包装器、适配器)

template<typename Callable, typename... Args>

auto safe_invoke(Callable&& f, Args&&... args) {

    try {

        return std::invoke(std::forward<Callable>(f), std::forward<Args>(args)...);

    } catch (...) {

        std::cerr << "Call failed!\n";

        throw;

    }

}

5. 与 std::apply 配合解包 tuple 调用

auto f = [](int a, double b, std::string c) {

    std::cout << a << ", " << b << ", " << c << "\n";

};

std::tuple<int, double, std::string> args{42, 3.14, "hello"};

std::apply(f, args); // 内部使用 invoke 实现

🔍 std::apply 的实现本质上是:

template<typename F, typename Tuple>

decltype(auto) apply(F&& f, Tuple&& t) {

    return std::invoke(f, std::get<Is>(std::forward<Tuple>(t))...);

}

⚠️ 注意事项

  • std::invoke

     不处理异常,调用失败会直接抛出。

  • 成员函数调用时,第一个参数必须是对象或指针,不能是引用(但指针会被自动解引用)。

  • 性能:零开销抽象,编译后通常内联为直接调用。


二、std::variant —— 类型安全的联合体

✅ 核心作用

有限的一组类型中,安全地存储其中一个类型的值。
替代传统的 union(无类型信息、不能有非平凡析构)和 void*(完全丧失类型安全)。

🛑 union 无法自动调用析构函数,std::variant 可以!

🔧 基本语法

#include <variant>

std::variant<T1, T2, ..., Tn> v;

📌 核心特性

特性

说明

类型安全

编译期知道可能类型,运行时知道当前类型

自动析构

切换值时自动析构旧对象

无堆分配

所有数据在栈上(大小 = max(sizeof(Ti)) + tag)

支持访问者模式

通过 std::visit 安全访问


🧪 常见操作与示例

1. 构造与赋值

std::variant<int, std::string, double> v;

v = 42;               // holds int

v = "hello"s;         // holds std::string(注意用 "s" 避免 char*)

v = 3.14;             // holds double

2. 检查当前类型

if (std::holds_alternative<int>(v)) {

    std::cout << "It's an int: " << std::get<int>(v) << "\n";

}

3. 安全获取值(带异常)

try {

    int x = std::get<int>(v); // 若不是 int,抛出 std::bad_variant_access

} catch (const std::bad_variant_access& e) {

    // handle error

}

4. 使用 std::visit 安全访问(推荐方式!)

std::visit([](const auto& val) {

    using T = std::decay_t<decltype(val)>;

    if constexpr (std::is_same_v<T, int>) {

        std::cout << "int: " << val << "\n";

    } else if constexpr (std::is_same_v<T, std::string>) {

        std::cout << "string: " << val << "\n";

    } else {

        std::cout << "other: " << val << "\n";

    }

}, v);

✅ std::visit 是访问 variant 的首选方式,避免手动类型检查。

5. 自定义访问者(Visitor Pattern)

struct Printer {

    void operator()(int i) const { std::cout << "int: " << i; }

    void operator()(const std::string& s) const { std::cout << "str: " << s; }

    void operator()(double d) const { std::cout << "dbl: " << d; }

};

std::visit(Printer{}, v);

6. 返回值的 visit(C++17 支持)

auto result = std::visit([](const auto& val) -> std::string {

    return std::to_string(val);

}, v);

⚠️ 所有分支必须返回相同类型(或可隐式转换)。


🚫 常见陷阱与规避

陷阱

解决方案

空 variant std::variant

 永远不会为空(除非 valueless_by_exception

异常导致 valueless

析构/移动抛异常时进入 valueless_by_exception 状态,应避免在 variant 类型中使用可能抛异常的移动操作

忘记处理所有类型

使用 std::visit + if constexpr 或完整 visitor

性能误解 sizeof(variant)

 = 最大类型 + 标记(通常 1 字节),无指针开销


🏗️ 典型应用场景

  1. 配置解析

    :配置项可能是 int / string / bool

  2. AST(抽象语法树)节点

    :不同节点类型

  3. 状态机状态

    :每个状态携带不同类型数据

  4. API 返回值

    :成功返回数据,失败返回错误码(可结合 expected,C++23 引入)

// 示例:JSON 值类型

using JsonValue = std::variant<

    std::monostate,  // null

    bool,

    int64_t,

    double,

    std::string,

    std::vector<JsonValue>,

    std::map<std::string, JsonValue>

    >;

三、std::invoke 与 std::variant 的结合使用

二者常配合实现动态分发

std::variant<int, std::string> v = "hello";

// 假设我们有一个函数表

struct Handler {

    void handle(int x) { std::cout << "Handling int: " << x; }

    void handle(const std::string& s) { std::cout << "Handling str: " << s; }

};

Handler h;

//使用 visit + invoke 实现成员函数分发

std::visit([&h](const auto& val) {

    std::invoke(&Handler::handle, h, val);

}, v);

这种模式在事件系统、命令分发、序列化中非常常见。


四、总结对比表

特性

std::invoke std::variant
目的

统一调用可调用对象

安全存储多类型之一

头文件 <functional> <variant>
是否拥有数据

关键优势

简化泛型调用逻辑

替代 unsafe union

典型用法

包装器、适配器、apply

状态机、AST、配置

性能

零开销(内联)

栈存储,无堆分配


五、最佳实践建议

✅ std::invoke

  • 在泛型库(如 task system、event dispatcher)中使用。

  • 优先于手写 (obj.*ptr)(args...)

  • 与 std::apply、完美转发结合使用。

✅ std::variant

  • 永远优先使用 std::visit 访问

    ,而非 get<T>

  • 避免在 variant 中放入可能抛异常的移动操作类型。

  • 若需要“可能无值”,考虑 std::optional<std::variant<...>>

  • 对于错误处理,未来可迁移到 C++23 的 std::expected


如需进一步探讨:

  • 如何实现 variant 的递归访问(如 JSON 打印)?

  • std::visit

     的多 variant 同时访问(C++17 支持)?

  • 与 std::any 的选型对比?

欢迎扫描关注,持续交流学习!! 

代码之外的风景:程序员如何平衡工作与生活的艺术

    

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐