C++(6)之function与成员函数


Author: Once Day Date: 2025年5月18日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…

漫漫长路,有人对你微笑过嘛…

全系列文章可参考专栏: 源码分析_Once-Day的博客-CSDN博客

参考文章:


1. 概述

std::function是C++11引入的一个通用多态函数封装器,是一个类模板。它可以封装各种可调用对象(Callable Objects),包括普通函数、函数指针、lamda表达式、仿函数以及成员函数等,使它们形成一个统一的函数调用接口。从而使得算法能够不依赖具体的函数类型,只要是满足特定调用形式的可调用对象即可。这大大提高了代码的灵活性和泛用性。

std::function的基本使用方法如下:

  1. 声明一个std::function对象,并指定其参数类型和返回值类型,形如std::function<返回值类型(参数类型列表)> 变量名
  2. 将各种可调用对象绑定到该std::function对象上。
  3. 像普通函数一样使用std::function对象进行调用。

下面是一些具体的代码示例:

}#include 
using namespace std;

// 普通函数
int foo(int a, int b) { return a + b; }

// lambda表达式
auto lambda = [](int a, int b) { return a + b; };

// 函数对象类
struct Functor {
	int operator()(int a, int b) { return a + b; }
};

// 类成员函数
struct Bar {
	int add(int a, int b) { return a + b; }
};

int main() {
 	// 封装普通函数
	function<int(int, int)> func1 = foo;
 
	// 封装lambda表达式
	function<int(int, int)> func2 = lambda;

	// 封装函数对象
	function<int(int, int)> func3 = Functor();

    // 封装类成员函数
    Bar bar;
    function<int(int, int)> func4 = bind(&Bar::add, &bar, placeholders::_1, placeholders::_2);

    // 调用
    int result1 = func1(1, 2);  // 1+2=3
    int result2 = func2(3, 4);  // 3+4=7  
    int result3 = func3(5, 6);  // 5+6=11
    int result4 = func4(7, 8);  // 7+8=15
    return 0;
}

可以看到,使用std::function后,所有不同类型的可调用对象都可以用统一的方式进行调用,非常方便。std::function对象与bind配合使用,可以很方便地将类成员函数转换成与普通函数一样的调用形式。

下面介绍一下std::function与其他几种函数对象的区别和联系:

  • 函数指针:函数指针可以指向一个普通函数,但无法直接保存lambda、仿函数等其他可调用对象。而std::function可以统一封装各类可调用对象。函数指针可以隐式转换为对应的std::function对象。
  • lambda表达式:lambda表达式是C++11新引入的匿名函数写法,可以方便地在需要时定义一个内联的函数对象。但lambda表达式的类型无法直接声明,必须借助std::function或者auto关键字。lambda表达式可以隐式转换为对应的std::function对象。
  • 仿函数:仿函数(Functor)是通过在类中重载()运算符模拟函数调用的类对象。一般在声明时就需要确定其参数和返回值类型。因此仿函数类型无法直接用于模板参数传递,而std::function对象可以。可以将仿函数对象赋给对应的std::function对象。
  • 成员函数指针:成员函数指针可以指向类的非静态成员函数,使用时需要提供一个该类的对象。无法直接用于一般化的函数回调。而std::function搭配bind/mem_fn可以将成员函数转换为与普通函数相同的调用形式,一般化为普通的函数回调,用起来更加灵活方便。

std::function是对各种可调用对象的一个统一封装,使它们拥有相同的函数调用形式。std::function对象可以像普通函数一样作为算法的模板参数传递,大大提高了代码的复用性和灵活性。其缺点是会带来少量的运行时开销。

2. function实现原理

std::function实际上是一个类模板,其内部维护了一个能够容纳各种可调用对象的内存缓冲区,以及一系列指向类型擦除接口的函数指针。不同类型的可调用对象在绑定到std::function时,会被复制到这个内存缓冲区中,并动态生成一个能够调用该对象的类型擦除接口,将接口的函数指针保存起来。这个过程叫做类型擦除(Type Erasure)。

所谓类型擦除,指的是在编译期将一组具有相同接口的类型抽象为一个统一的类型,从而实现运行期的多态调用。std::function就是通过类型擦除技术,将不同类型的可调用对象抽象为统一的调用接口,实现了对不同函数对象的多态调用。

假设我们先后将一个普通函数指针fp和一个lambda表达式lam绑定到同一个std::function对象func中:

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

auto lam = [](int a, int b) { return a+b; };

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

func = foo;  // 绑定函数指针
func(1, 2);  // 调用函数指针

func = lam;  // 绑定lambda
func(3, 4);  // 调用lambda

对func的第一次赋值,会将函数指针foo复制到func的内存缓冲区中,并动态生成一个调用该函数指针的类型擦除接口,大致如下:

// func内部生成的类型擦除接口 - 调用函数指针
int invoke(void* fp, int a, int b) {
    return (*(int(*)(int,int))fp)(a, b);
}

对func的第二次赋值,会将lambda表达式lam复制到func的内存缓冲区中(覆盖之前的foo),并动态生成一个调用该lambda的新的类型擦除接口,大致如下:

// func内部生成的类型擦除接口 - 调用lambda
int invoke(void* lam, int a, int b) {
    return (*(lambda类型*)lam)(a, b); 
}

后续对func的调用,就会通过函数指针invoke,跳转到绑定的函数对象那里去执行。

std::function通过在内部维护一个原始内存缓冲区来存储绑定的可调用对象,再结合类型擦除技术动态生成调用被绑定对象的统一接口,从而实现了对不同类型可调用对象的封装与多态调用。这种实现带来了一定的空间和时间开销,但提供了非常灵活方便的泛型编程手段,是一种空间换时间的trade-off.

std::function的实际实现要比上面描述的复杂得多,涉及到SBO(small buffer optimization)、移动语义、引用计数等诸多复杂的编程技术,但基本原理就是类型擦除+多态调用。

3. 成员函数转换

std::function一种典型使用是与类成员函数结合,在基类和派生类之间实现成员函数指针的调用接口自动转换(类指针偏移地址自动修正)。下面是一段示例代码:

#include <stdint.h>

#include <cxxabi.h>
#include <functional>
#include <string>
#include <unordered_map>

// 获取类型信息
template < typename T >
static const char *GetTypeName()
{
    // 获取类型信息
    const char *type_name = typeid(T).name();
    // 使用CXXABI获取函数指针
    const char *name = abi::__cxa_demangle(type_name, NULL, NULL, NULL);
    return name;
}

// 打印成员变量信息
template < typename MemberType >
static void PrintMemberVariableInfo(MemberType &member, const char *name)
{
    uint64_t *data = reinterpret_cast< uint64_t * >(&member);
    printf("    MemberVar : name %-28s, address %p, value 0x%lx, type %s\n", name, data, data[0],
        GetTypeName< MemberType >());
}

// 打印成员函数对象信息
template < typename MemberFuncType >
static void PrintMemberFunctionInfo(MemberFuncType &func, const char *name)
{
    const uint64_t *data = reinterpret_cast< const uint64_t * >(&func);
    printf("    MemberFunc: name %-28s, object %p, func 0x%lx, (this*) offset 0x%lx, type %s\n",
        name, data, data[0], data[1], GetTypeName< MemberFuncType >());
}

// 打印成员函数对象信息
template < typename MemberFuncType >
static void PrintMemberFunctionInfo2(MemberFuncType &func, const char *name)
{
    const uint64_t *data = reinterpret_cast< const uint64_t * >(&func);
    printf("        MemberFunc: name %-28s, object %p, func 0x%lx, (this*) offset 0x%lx, type %s\n",
        name, data, data[0], data[1], GetTypeName< MemberFuncType >());
}

// 打印 std::function 对象信息
template < typename FuncType >
static void PrintFunctionObjectInfo(std::function< FuncType > &func, const char *name)
{
    // 打印参数信息
    const uint64_t *data = reinterpret_cast< const uint64_t * >(&func);
    printf("    FuncObject: name %-28s, object %p, func 0x%lx, (this*) offset 0x%lx, type %s\n",
        name, data, data[0], data[1], GetTypeName< FuncType >());
}

// 使用给定参数调用函数对象
template < typename FuncType, typename DataType >
static int32_t CallFunctionObject(FuncType &func, DataType &data)
{
    // 打印参数信息
    printf(
        "      call converter with data: address %p, type: %s\n", &data, GetTypeName< DataType >());

    // 调用函数对象
    return func(data);
}

class IfaceConfig {
public:
    void SetIfaceData(uint64_t data)
    {
        iface_data_ = data;
        printf("          IfaceConfig - Set iface data: %lu, address %p == %p [%s]\n", iface_data_,
            &iface_data_, iface_data_address,
            &iface_data_ == iface_data_address ? "true" : "false");
    }

    /* private: */
    uint64_t         iface_data_;
    static uint64_t *iface_data_address;
};

uint64_t *IfaceConfig::iface_data_address = nullptr;

class EtherConfig {
public:
    void SetEtherData(uint64_t data)
    {
        ether_data_ = data;
        printf("          EtherConfig - Set ether data: %lu, address %p == %p [%s]\n", ether_data_,
            &ether_data_, ether_data_address,
            &ether_data_ == ether_data_address ? "true" : "false");
    }

    /* private: */
    uint64_t         ether_data_;
    static uint64_t *ether_data_address;
};

uint64_t *EtherConfig::ether_data_address = nullptr;

class PhysicalIfaceConfig : public IfaceConfig, public EtherConfig {
public:
    void SetPhysicalData(uint64_t data)
    {
        physical_data_ = data;
        printf("          PhysicalConfig - Set physical data: %lu, address %p == %p [%s]\n",
            physical_data_, &physical_data_, physical_data_address,
            &physical_data_ == physical_data_address ? "true" : "false");
    }

    /* private: */
    uint64_t         physical_data_;
    static uint64_t *physical_data_address;
};

uint64_t *PhysicalIfaceConfig::physical_data_address = nullptr;

// 定义一个函数指针类型, 用于转换节点数据
template < typename DataType >
using NodeConverter = std::function< int32_t(DataType &data) >;

// 物理接口配置节点数据转换器映射表
using PhysicalNodeConverter    = NodeConverter< PhysicalIfaceConfig >;
using PhysicalNodeConverterMap = std::unordered_map< std::string, PhysicalNodeConverter >;

// 模版函数 - 获取节点的值并赋值保存
template < typename DataType, typename FieldType >
static NodeConverter< DataType > MakeNodeConverter(void (DataType::*set_value)(FieldType))
{
    return [set_value](DataType &data) -> int32_t {
        FieldType temp = 8848;

        // 打印 data 地址
        printf("        NodeConverter - data: %p => %s\n", &data, GetTypeName< DataType >());
        // 打印参数信息
        PrintMemberFunctionInfo2(set_value, "NodeConverter::SetFunc");
        // 直接赋值 - 通过 SetFunc 函数进行赋值
        (data.*set_value)(temp);

        return 0;
    };
}

int main(void)
{
    PhysicalIfaceConfig physical = {};
    IfaceConfig        &Iface    = physical;
    EtherConfig        &ether    = physical;

    // 初始化地址信息
    IfaceConfig::iface_data_address            = &physical.iface_data_;
    EtherConfig::ether_data_address            = &physical.ether_data_;
    PhysicalIfaceConfig::physical_data_address = &physical.physical_data_;

    // EtherConfig::SetEtherData 测试
    printf("\nEtherConfig::SetEtherData\n");
    printf("  Member function info:\n");
    auto set_ether_data_auto                              = &PhysicalIfaceConfig::SetEtherData;
    void (PhysicalIfaceConfig::*set_ether_data)(uint64_t) = &PhysicalIfaceConfig::SetEtherData;
    PrintMemberFunctionInfo(set_ether_data_auto, "set_ether_data_auto");
    PrintMemberFunctionInfo(set_ether_data, "set_test_data2");
    printf("  Function object info:\n");
    auto ether_converter_auto = MakeNodeConverter(set_ether_data_auto);
    PrintFunctionObjectInfo(ether_converter_auto, "ether_converter_auto");
    auto ether_converter = MakeNodeConverter(set_ether_data);
    PrintFunctionObjectInfo(ether_converter, "ether_converter");

    // 生成映射表
    printf("\nGenerate physical_converter_map:\n");
    PhysicalNodeConverterMap physical_converter_map;
    physical_converter_map["ether_converter_auto"] = ether_converter_auto;
    physical_converter_map["ether_converter"]      = ether_converter;
    auto iter5 = physical_converter_map.find("ether_converter_auto");
    auto func5 = iter5->second;
    PrintFunctionObjectInfo(func5, "PhysicalMap::ether_converter_auto");
    CallFunctionObject(func5, physical);
    printf("\n");
    auto iter6 = physical_converter_map.find("ether_converter");
    auto func6 = iter6->second;
    PrintFunctionObjectInfo(func6, "PhysicalMap::ether_converter");
    CallFunctionObject(func6, physical);

    return 0;
}

下面是这段代码的关键部分解析:

  • 类的继承关系:PhysicalIfaceConfig继承自IfaceConfig和EtherConfig,形成了多重继承的关系。在main函数中,创建了一个PhysicalIfaceConfig对象physical,并用其对IfaceConfig和EtherConfig部分进行了切片。
  • 成员函数指针的使用:代码中定义了两个成员函数指针set_ether_data_auto和set_ether_data,分别使用auto自动推导和显式声明的方式,指向了PhysicalIfaceConfig::SetEtherData函数。使用PrintMemberFunctionInfo函数打印成员函数指针的信息,可以看到它们都指向同一个函数,只是类型不同而已。
  • std::function对象的绑定:使用MakeNodeConverter函数模板,将成员函数指针绑定到std::function对象ether_converter_auto和ether_converter上。其中,MakeNodeConverter的模板参数DataType为PhysicalIfaceConfig,FieldType为uint64_t,返回值类型为NodeConverter。std::function对象的类型为:std::function<int32_t(PhysicalIfaceConfig&)>。
  • 类型擦除与多态调用:将ether_converter_auto和ether_converter插入到PhysicalNodeConverterMap中,得到一个统一的std::function类型,实现了类型擦除。在后续的查找和调用中,通过map的key值获取对应的std::function对象,再通过统一的调用方式CallFunctionObject来执行,实现了多态调用。
  • 打印调试信息:代码中大量使用了打印函数,如PrintMemberFunctionInfo、PrintFunctionObjectInfo等,来打印成员函数指针、std::function对象的内部信息,如函数地址、this指针偏移等,可以帮助理解和调试。此外还打印了传递给std::function的physical对象的信息,可以看到std::function确实捕获了physical对象,并在调用时将其作为this指针传递给了成员函数。
  • 指针的特殊用法:代码中使用了两个static成员变量iface_data_address和ether_data_address来记录physical对象中,基类子对象的成员变量地址。目的是为了验证在多态调用时,传递给std::function的physical对象,其中基类子对象的地址与直接访问physical时是一致的,印证了多态调用的正确性。

这段代码运行结果如下:

在这里插入图片描述

从输出信息来看,基类对象的指针偏移记录在成员函数指针对象上,所以PhysicalIfaceConfig::SetEtherData会有一个0x8的偏移量,但EtherConfig::SetEtherData没有偏移量。

把EtherConfig::SetEtherData赋值给ether_converter后,会自动生成一个新的function函数对象,并且自动把PhysicalConfig类指针偏移量加上8个字节,然后再执行目标函数,相当于自动完成PhysicalConfig到EtherConfig的向上转换。

PhysicalIfaceConfig::SetEtherData则不一样,function对象传递过程没有发生任何变化,在成员函数执行时,自动添加8个字节的偏移量。

更多推荐