[how does it work series] std::bind

发布时间 2023-12-25 22:39:46作者: sherlock2001

本文不是一篇对std::bind的源码分析,而是试图通过逐步推导的方式,不断迭代优化,最终实现一版能阐述清核心原理的demo。非常像真实的开发过程。

事实上,关于std::bind的源码分析已有优质的讲解,建议想深入了解的读者参阅。

什么是std::bind?

std::bind 是 C++ 标准库中的一个函数模板,它用于创建一个可调用对象(callable object)。通过 std::bind,可以将一个函数与其参数绑定为一个可调用对象,从而延迟函数的调用或者改变函数的调用方式。下面是个例子:

#include <iostream>
#include <functional>

void printSum(int a, int b) {
    std::cout << "Sum: " << a + b << std::endl;
}

int main() {
    auto bindFunc = std::bind(printSum, 10, 20);
    bindFunc(); // 调用绑定的函数
    return 0;
}

推导过程

明确目的

bind函数接受一个callable实例(可能是函数指针、lambda或functor等),以及一系列不定参数;返回一个有着新签名(signature)的callable实例。据此我们可以给出第一版设计(注意并不能编译):

template<typename TFunc, typename ...TArgs>
struct bind_result {
  explicit bind_result(TFunc func, TArgs ...args)
    : func(std::move(func)), args(args...)
  {}

  template<typename ...TRealArgs>
  void operator()(TRealArgs ...real_args) {
    func(args.../* not compile */, real_args...);
  }

 private:
  TFunc func;
  std::tuple<TArgs...> args;
};

问题在于我们无法直接申明模板函数包(template argument pack)类型的成员变量,只能用std::tuple包一层;而std::tuple无法使用折叠表达式(fold expression)语法来展开。这样导致注释处出现编译错误。我们需要解决展开std::tuple的问题。

展开tuple

恰巧笔者在SO上读到这篇文章,里面提供了将tuple展开的思路,先给出实现,再进行解释。

template<int ...i>
struct seq {};

template<int i, int ...pack>
struct gen_seq {
  using type = typename gen_seq<i - 1, i - 1, pack...>::type;
};

template<int ...pack>
struct gen_seq<0, pack...> {
  using type = seq<pack...>;
};

下面对该trait举例说明。考虑gen_seq<3>这个例子,其模板参数列表(template<int i, int ...pack>)中的i等于3,pack为空,那么using type = typename gen_seq<i - 1, i - 1, pack...>::type会被解释为using type = gen_seq<2, 2>::type。以此类推,gen_seq<2, 2>::type会被最终解释为seq<0, 1, 2>

以下例子展示了展开tuple的全流程,理解此例子有助于理解后面的代码:

template<int ...i>
void print_i(seq<i...>) {
  /* fold expression in C++17 */
  (printf("%d ", i), ...);
}

int main() {
  print_i(gen_seq<3>::type{});   // (1)
}

解释:标注(1)处利用gen_seq创建seq<0,1,2>实例,该实例变量又指导编译器推理出模板参数,最终利用折叠表达式(fold expression)逐个将0,1,2在console中输出。

阶段性成果

更新bind_result,解决编译错误,我们可以得到一个能work的阶段性结果。值得一提的是,我们的bind尚且存在许多局限性,还无法应用于生产环境,后面的文章会继续优化。个人觉得不断解决新的挑战、不断重构才是编程的乐趣。

template<typename TFunc, typename ...TArgs>
struct bind_result {
  explicit bind_result(TFunc func, TArgs ...args)
    : func(std::move(func)), args(args...)
  {}

  template<typename ...TRealArgs>
  void operator()(TRealArgs ...real_args) {
    internal_call(typename gen_seq<sizeof...(TArgs)>::type(), real_args...);
  }

 private:
  TFunc func;
  std::tuple<TArgs...> args;

 private:

  template<int ...i, typename ...TRealArgs>
  void internal_call(seq<i...>, TRealArgs ...real_args) {
    func(std::get<i>(args)..., real_args...);
  }

};

测试样例1(绑定lambda -> OK):

int main() {
  auto f = [](int i, int j){
    std::cout << "i: " << i << "; j: " << j;
  };

  bind_result br(f, 1);
  br(2);
}

测试样例2(绑定function -> OK):

void f(int i, int j) {
  std::cout << "i: " << i << "; j: " << j;
}

int main() {
  bind_result br(&f, 1);
  br(2);
}

测试样例3(绑定method -> Err):

struct foo {
  void f(int i, int j) {
    std::cout << "i: " << i << "; j: " << j;
  }
};

int main() {
  bind_result br(&foo::f, 1);
  br(2);  // unlike other managed language, member func can't be simply invoked like
          // a function pointer, otherwise, you should invoke it with an instance 
          // variable.
}

测试样例4(绑定带返回值的callable对象 -> Err):

int main() {
  auto f_sum = [](int i, int j){
    return i + j;
  };
  bind_result f_sum_by_1(f_sum, 1);

  auto res = f_sum_by_1(2);  // f_sum_by_1 returns void.
}