C++ STL笔记01-std::function容器

 

动手实现C++ STL容器。本节介绍std::function的常见用法和实现。

std::function用法

std::function是一种通用、多态的函数封装。它允许对可调用目标实体进行存储、复制、和调用操作,而这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。

利用std::function我们可以更容易地操作C++中的函数。比如说我们可以使用std::function对象来包装函数(包括Labmda表达式、函数指针、结构体等),并实现函数调用:

#include <stdio.h>
#include <functional>

void func1(int x) {
    printf("%d from function\n", x);
}

struct func_struct {
    void operator() (int x) {
        printf("%d from func_struct\n", x);
    }
    
    static void foo(int x) {
        printf("%d from static member function\n", x);
    }
};

int main()
{
    // 函数
    std::function<void(int)> func;
    func = func1;
    func(1);
    
    // 函数指针
    void (*func_ptr)(int);
    func_ptr = func1;
    func = func_ptr;
    func(2);
    
    // lambda表达式
    auto func2 = [](int x) {printf("%d from lambda expression\n", x);};
    func = func2;
    func(3);
    
    // 结构体(函数对象)
    func = func_struct();
    func(4);
    
    // 静态成员函数
    func = func_struct::foo;
    func(5);

    return 0;
}

运行上面的代码可以得到如下结果:

1 from function
2 from function
3 from lambda expression
4 from func_struct
5 from static member function

Function实现

接下来我们从零开始实现一个类似于标准库中std::function的函数容器Function。首先是模板定义:

template <class FnSig>
struct Function {
    // 只在使用了不符合 Ret(Args...) 模式的 FnSig 时出错
    static_assert(!std::is_same_v<FnSig, FnSig>, "not a valid function signature");
};

template <class Ret, class ...Args>
struct Function<Ret(Args...)> {
    ...
}

这里Function模板有两个定义:第一个的定义是一个通用定义;而第二个的定义则是通用定义的模板特化,用来接收<Ret(Args...)>形式的模板参数。这样做的意义在于只有当我们使用了<Ret(Args...)>形式的模板参数才会生成具体的代码,否则会并在编译期报错。

需要额外说明的是模板参数<Ret(Args...)>的意义:

  • Ret表示函数的返回类型
  • Args...表示一系列函数参数类型,其中省略号...说明可以接受零个或多个参数并且构成一个参数包

因此<Ret(Args...)>实际上是一个用于表示函数的返回类型和参数类型的函数签名。

下面看一下Function<Ret(Args...)>的具体实现:

template <class Ret, class ...Args>
struct Function<Ret(Args...)> {
private:
    struct FuncBase {
        ...
    };

    template <class F>
    struct FuncImpl : FuncBase {
        ...
    };

    std::shared_ptr<FuncBase> m_base;

public:
    Function() = default; // m_base 初始化为 nullptr

    // 此处 enable_if_t 的作用:阻止 Function 从不可调用的对象中初始化
    template <class F, class = std::enable_if_t<std::is_invocable_r_v<Ret, F &, Args...>>>
    Function(F f) // 没有 explicit,允许 lambda 表达式隐式转换成 Function
    : m_base(std::make_shared<FuncImpl<F>>(std::move(f)))
    {}

    Ret operator()(Args ...args) const {
        if (!m_base) [[unlikely]]
            throw std::runtime_error("function not initialized");
        // 完美转发所有参数,这样即使 Args 中具有引用,也能不产生额外的拷贝开销
        return m_base->call(std::forward<Args>(args)...);
    }
};

首先,Function中包含一个指针m_base用来指向具体的函数对象实例。m_base的类型是FuncBase,它是一个函数基类我们先略过它的具体定义。

struct Function<Ret(Args...)> {
private:
...
    std::shared_ptr<FuncBase> m_base;
...
}

构造函数Function(F f)接收一个函数对象f作为参数并使用m_base来存储这个对象,此时会使用FuncImpl来对F类型进行封装。这里FuncImpl是函数基类FuncBase的实现,我们同样先略过它。需要注意的是在构造函数中还使用了std::enable_if以及std::is_invocable_r_v来判断F类型是否可以调用并且满足函数签名<Ret(Args...)>,只有满足条件的类型才能进行初始化。

public:
    ...
    // 此处 enable_if_t 的作用:阻止 Function 从不可调用的对象中初始化
    template <class F, class = std::enable_if_t<std::is_invocable_r_v<Ret, F &, Args...>>>
    Function(F f) // 没有 explicit,允许 lambda 表达式隐式转换成 Function
    : m_base(std::make_shared<FuncImpl<F>>(std::move(f)))
    {}
    ...

除此之外Function还重载了函数调用运算符,当m_base非空时通过m_base来执行实际的函数调用。

public:
...
    Ret operator()(Args ...args) const {
        if (!m_base) [[unlikely]]
            throw std::runtime_error("function not initialized");
        // 完美转发所有参数,这样即使 Args 中具有引用,也能不产生额外的拷贝开销
        return m_base->call(std::forward<Args>(args)...);
    }
...

最后我们来看一下FuncBaseFuncImpl的实现:

    struct FuncBase {
        virtual Ret call(Args ...args) = 0; // 类型擦除后的统一接口
        virtual ~FuncBase() = default;      // 应对F可能有非平凡析构的情况
    };

    template <class F>
    struct FuncImpl : FuncBase { // FuncImpl 会被实例化多次,每个不同的仿函数类都产生一次实例化
        F m_f;

        FuncImpl(F f) : m_f(std::move(f)) {}

        virtual Ret call(Args ...args) override {
            // 完美转发所有参数给构造时保存的仿函数对象
            return m_f(std::forward<Args>(args)...);
            // 更规范的写法其实是:
            // return std::invoke(m_f, std::forward<Args>(args)...);
            // 但为了照顾初学者依然采用朴素的调用方法
        }
    };

FuncBase是一个虚拟基类,它提供一个虚函数call作为统一的接口;而FuncImpl则是FuncBase的具体实现,当我们调用Function时,实际上是在调用FuncImpl实现的call函数。

总结一下,整个Function的实现可以参考如下:

#pragma once

#include <iostream>
#include <utility>
#include <stdexcept>
#include <memory>
#include <type_traits>
#include <functional>

template <class FnSig>
struct Function {
    // 只在使用了不符合 Ret(Args...) 模式的 FnSig 时出错
    static_assert(!std::is_same_v<FnSig, FnSig>, "not a valid function signature");
};

template <class Ret, class ...Args>
struct Function<Ret(Args...)> {
private:
    struct FuncBase {
        virtual Ret call(Args ...args) = 0; // 类型擦除后的统一接口
        virtual ~FuncBase() = default;      // 应对F可能有非平凡析构的情况
    };

    template <class F>
    struct FuncImpl : FuncBase { // FuncImpl 会被实例化多次,每个不同的仿函数类都产生一次实例化
        F m_f;

        FuncImpl(F f) : m_f(std::move(f)) {}

        virtual Ret call(Args ...args) override {
            // 完美转发所有参数给构造时保存的仿函数对象
            return m_f(std::forward<Args>(args)...);
            // 更规范的写法其实是:
            // return std::invoke(m_f, std::forward<Args>(args)...);
            // 但为了照顾初学者依然采用朴素的调用方法
        }
    };

    std::shared_ptr<FuncBase> m_base; // 使用智能指针管理仿函数对象,用shared而不是unique是为了让Function支持拷贝

public:
    Function() = default; // m_base 初始化为 nullptr

    // 此处 enable_if_t 的作用:阻止 Function 从不可调用的对象中初始化
    template <class F, class = std::enable_if_t<std::is_invocable_r_v<Ret, F &, Args...>>>
    Function(F f) // 没有 explicit,允许 lambda 表达式隐式转换成 Function
    : m_base(std::make_shared<FuncImpl<F>>(std::move(f)))
    {}

    Ret operator()(Args ...args) const {
        if (!m_base) [[unlikely]]
            throw std::runtime_error("function not initialized");
        // 完美转发所有参数,这样即使 Args 中具有引用,也能不产生额外的拷贝开销
        return m_base->call(std::forward<Args>(args)...);
    }
};

Function在进行实例化时,具有相同签名<Ret(Args...)>的只会实例化出一份代码。因此只要可调用对象具有相同的签名,我们就可以使用同一个Function类来进行处理;而对于具有相同签名但不同类型的函数对象,编译器会实例化出相应的FuncImpl类而无需重新实例化整个Function类。这种机制称为类型擦除,它允许在不同函数对象签名下实现通用性,同时保持类型安全和高效性。

Reference