【c++】递归lambda式

原文:zhihu

首先,什么是lambda

是闭包,是函数对象(重载了operator()的类型),是一种持有状态的函数

当写下

auto a = 2;
auto f = [&](int x) {return a * x;};

时,编译器会把代码翻译成大概下面这样,生成一个匿名类(https://cppinsights.io/翻译的结果)

int a = 2;

class __lambda_6_12
{
    public: 
    inline /*constexpr */ int operator()(int x) const
    {
        return a * x;
    }

    private: 
    int & a;

    public:
    __lambda_6_12(int & _a)
        : a{_a}
    {}

};

__lambda_6_12 f = __lambda_6_12{a};

lambda的优点在于简化了函数对象的编码,把函数对象定义在了调用处,同时减少了命名(无需类型命名,只需命名变量或无需命名),如果不打算重用函数,比如std::thread构造函数传入的线程运行函数,可以考虑lambda

但如果lambda一定需要递归实现呢?

比如说下文中我们想实现一个递归打印数字的count_down函数,其中分隔符sep不是固定的

对于常规函数与函数对象(闭包),可以维护一个全局变量或成员变量sep

std::string sep = get_sep();
auto count_down(int x) -> std::string  
    return x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1);
}

或者把sep作为一个额外形参

auto count_down(int x, std::string const& sep) -> std::string { 
    return  x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1, sep);
}

而对于lambda,我们可以首先尝试一下把lambda式变量名按引用捕获到lambda函数体里面

std::string sep = get_sep();
auto count_down = [&](int x) -> std::string { 
    return  x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1);
};

oops.. 编译失败了,报错是

error: variable 'count_down' declared with deduced type 'auto' cannot appear in its own initializer

因为我们需要把右侧匿名的lambda赋值给左侧auto的变量count_down,而对于编译器,需要解析完赋值表达式的右侧才能确定auto对应的lambda类型,所以我们在函数体里使用类型未定的count_down自身时,很不幸,编译器拒绝了类型推导

所以问题在于在递归调用时需要确定lambda本身的类型

第一种解决方案,使用std::function

std::string sep{", "}
std::function<std::string(int)> count_down = [&](int x) -> std::string { 
    return  x == 0 ? "0" : std::to_string(x) + sep + count_down(x - 1);
};
cout << count_down(5);
//5, 4, 3, 2, 1, 0

可以编译了的原因是std::function对象内部存储了lambda可调用对象,std::function调用operator()时,会把实参完美转发到内部的可调用对象,由于指定了std::function的模板参数类型,我们回避了匿名类型的问题

然而这里有两个小问题

  1. 由于lambda存在function里,递归的调用链是function->lambda->function->lambda->...
  2. 函数签名重复写了两次,太丑了

好在C++14支持了泛型lambda,回想下问题在于在lambda递归调用时需要确定本身的类型,那我们其实只给lambda多加一个指向自身的泛型形参就好了,实现如下

 auto count_down = [impl = [&](auto const &self, int x) -> std::string {
    return x == 0 ? "0" : std::to_string(x) + sep + self(self, x - 1);
}](auto &&...args) {
    return impl(impl, std::forward<decltype(args)>(args)...);
};

思路就是把lambda式存在另一个闭包里,闭包调用时,调用实际的递归函数,同时把它自身的引用作为第一个参数,这样实际上lambda对象只是不断递归调用自身

当然这么写还是麻烦,我们把生成新闭包和转发实参的部分也写成一个如下的泛型lambda(函数装饰器)

 auto recursive_lambda = [](auto &&lam) {
     return [lam_impl = std::forward<decltype(lam)>(lam)](auto &&...args) {
         return lam_impl(lam_impl, std::forward<decltype(args)>(args)...);
     };
 };

这样就实现了递归lambda

std::string sep{"..."};  
auto count_down = recursive_lambda([&](auto const &self, int x) -> std::string  { 
    return x == 0 ? "0" : std::to_string(x) + sep + self(self, x - 1);
});
cout << count_down(5);
//10...9...8...7...6...5...4...3...2...1...0 

注意上述recursive_lambda使用时,如果返回类型不是void,需要在lambda形参之后,函数体之前使用->尾置返回类型