3.1、std::invoke

为什么要有std::invoke?

在C++17之前,调用不同类型的可调用对象(普通函数、函数指针、成员函数指针、函数对象、lambda等)需要用不同语法:

  • • 普通函数或函数指针用f(args...)
  • • 类成员函数指针用(obj.*pmf)(args...)或者(pobj->*pmf)(args...)
  • • 访问成员变量指针用obj.*pmdpobj->*pmd
  • • 函数对象和lambda用f(args...)

这导致泛型代码写起来非常繁琐,必须针对不同情况写不同调用代码,代码重复且难维护。

std::invoke的设计哲学就是统一调用接口,让你用同一种语法调用任何可调用对象,不用关心它们具体是什么类型。它是泛型编程的利器,极大简化了模板代码的复杂度,提高了代码的通用性和可读性。

std::invoke的核心功能和语法

std::invoke是一个函数模板,定义大致如下:

template<typename Callable, typename... Args>
decltype(autoinvoke(Callable&& f, Args&&... args);

它接受一个可调用对象f和若干参数args...,根据f的类型自动选择正确的调用方式,并返回调用结果。

支持调用的对象类型包括:

  • • 普通函数和函数指针
  • • 类成员函数指针
  • • 类成员变量指针
  • • 函数对象(仿函数)
  • • Lambda表达式
  • • std::function对象

调用示例:

struct Foo {
    int data = 42;
    int add(int x) return data + x; }
};

Foo foo;
auto res1 = std::invoke(&Foo::add, foo, 8);    // 调用成员函数
auto res2 = std::invoke(&Foo::data, foo);      // 访问成员变量
auto res3 = std::invoke([](int a, int b){ return a * b; }, 34); // 调用lambda

底层原理简析

std::invoke的实现依赖于模板元编程和SFINAE技术,通过判断传入的Callable类型,选择最合适的调用方式。它大致分为三种调用模式:

  1. 1. 成员函数指针调用
    如果Callable是成员函数指针,且第一个参数是对象或指针,调用方式为(obj.*pmf)(args...)(pobj->*pmf)(args...)
  2. 2. 成员变量指针访问
    如果Callable是成员变量指针,且第一个参数是对象或指针,调用方式为obj.*pmdpobj->*pmd
  3. 3. 普通函数或函数对象调用
    其他情况,直接使用f(args...)调用。

此外,std::invoke还支持完美转发参数,保证传入参数的值类别(左值/右值)不被破坏,返回类型也会保持正确的引用或值类型。

深度案例讲解

下面通过一个综合案例,带你深入理解std::invoke的高级用法和底层细节:

#include <iostream>
#include <functional>

struct Widget {
    int value = 10;
    int multiply(int x) return value * x; }
    static int static_add(int a, int b) return a + b; }
};

struct Functor {
    int operator()(int x, int y) const return x - y; }
};

int free_function(int x, int y) {
    return x + y;
}

int main() {
    Widget w;

    // 调用成员函数指针
    int res1 = std::invoke(&Widget::multiply, w, 5);
    std::cout << "res1 (成员函数): " << res1 << "\n"// 50

    // 调用成员变量指针
    int res2 = std::invoke(&Widget::value, w);
    std::cout << "res2 (成员变量): " << res2 << "\n"// 10

    // 调用静态成员函数
    int res3 = std::invoke(&Widget::static_add, 37);
    std::cout << "res3 (静态成员函数): " << res3 << "\n"// 10

    // 调用普通函数
    int res4 = std::invoke(free_function, 46);
    std::cout << "res4 (普通函数): " << res4 << "\n"// 10

    // 调用函数对象
    Functor f;
    int res5 = std::invoke(f, 94);
    std::cout << "res5 (函数对象): " << res5 << "\n"// 5

    // 调用lambda表达式
    auto lambda = [](int a, int b) { return a * b + 1; };
    int res6 = std::invoke(lambda, 23);
    std::cout << "res6 (lambda): " << res6 << "\n"// 7

    // 通过指针调用成员函数
    Widget* pw = &w;
    int res7 = std::invoke(&Widget::multiply, pw, 4);
    std::cout << "res7 (指针调用成员函数): " << res7 << "\n"// 40

    // 通过智能指针调用成员函数
    std::shared_ptr<Widget> spw = std::make_shared<Widget>();
    int res8 = std::invoke(&Widget::multiply, spw, 3);
    std::cout << "res8 (智能指针调用成员函数): " << res8 << "\n"// 30

    return 0;
}

代码细节解析

  • • 成员函数指针调用时,std::invoke自动判断第一个参数是对象、指针还是智能指针,内部通过operator->访问成员函数,极大简化了调用代码。
  • • 成员变量指针调用时,std::invoke返回成员变量的值,避免了手写obj.*pmd的繁琐。
  • • 静态成员函数和普通函数调用本质相同,直接调用即可。
  • • 函数对象和lambda调用时,std::invoke相当于调用它们的operator()
  • • 支持智能指针调用成员函数,std::invoke内部使用std::pointer_traitsoperator->,保证兼容性。
  • • 参数完美转发,保证传入参数的值类别不丢失,避免不必要的拷贝。

这个案例涵盖了std::invoke的绝大多数使用场景,掌握它就能在泛型编程中轻松应对各种可调用对象。

常见错误与误区

  1. 1. 忘记传递对象实例
    调用成员函数指针时,必须传入对象或指针,否则编译器会报错。
  2. 2. 传递对象时未使用引用或指针
    对象传递时,如果是临时对象或右值,可能导致调用失败或副本调用,建议使用引用或智能指针。
  3. 3. 成员变量指针调用时误用参数
    访问成员变量时,只需传入对象,无需额外参数。
  4. 4. 线程传参时未用std::ref导致std::invoke错误
    传递引用参数给线程函数时,必须用std::ref包裹,避免编译器误判。
  5. 5. 误用std::invoke与std::function
    std::invoke是调用工具,std::function是可调用对象的封装器,两者职责不同,不能混淆。

面试中可能出现的问题

  1. 1. 解释std::invoke的设计动机和解决了什么问题。
  2. 2. 如何用std::invoke统一调用成员函数指针和普通函数指针。
  3. 3. std::invoke如何支持智能指针调用成员函数。
  4. 4. std::invokestd::function的区别和联系。
  5. 5. 结合代码写出使用std::invoke调用成员变量指针的示例。
  6. 6. 说明std::invoke如何实现完美转发,为什么重要。

总结与独到见解

std::invoke不仅仅是语法糖,它是C++17对可调用对象调用机制的统一抽象,体现了现代C++设计中“接口统一、泛型优先、完美转发”的哲学。它让泛型代码不再为调用细节分心,专注于业务逻辑本身。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
(加入我的知识星球,免费获取账号,解锁所有文章。)

 

阅读剩余
THE END