C++06_函数式编程

函数也是对象

函数可以作为另一个函数的参数:

#include <cstdio>

template <class Func>
void call_wait(Func func) {
    func(0);
    func(1);
}

int main() {
    auto myFunc = [](int n) {
        printf("Number %d\n", n);
    };
    call_wait(myFunc);
    return 0;
}

并且,这个作为参数的函数也可以有参数:

#include <cstdio>

void print_number(int n) {
    printf("Number %d\n", n);
}

void call_twice(void func(int)) {
    func(0);
    func(1);
}

int main() {
    call_twice(print_number);
    return 0;
}

函数作为模板类型

甚至可以直接将 func 的类型作为一个模板参数,从而不需要写 void(int)。
这样还会允许函数的参数类型为其他类型,比如 void(float)。
这样 call_twice 会自动对每个不同的 func 类型编译一遍,从而允许编译器更好地进行自动适配与优化。

#include <cstdio>

void print_float(float f) {
    printf("Float %f\n", f);
}

void print_int(int n) {
    printf("Int %d\n", n);
}

template <class Func>
void call_twice(Func func) {
    func(0);
    func(1);
}

int main() {
    call_twice(print_float);
    call_twice(print_int);
    return 0;
}

lambda表达式

C++11 引入的 lambda 表达式允许我们在函数体内创建一个函数,大大地方便了函数式编程。
语法就是先一个空的 [],然后是参数列表,然后是 {} 包裹的函数体。
再也不用被迫添加一个全局函数了!

int main() {
    auto myFunc = [](int n) {
        printf("Number %d\n", n);
    };
    call_wait(myFunc);
    return 0;
}

lambda表达式:返回类型、自动推导返回类型

lambda 表达式的返回类型写在参数列表后面,用一个箭头 -> 表示。

int main() {
    auto twice = [](int n) -> int {
        return n * 2;
    };
    call_twice(twice);
    return 0;
}

如果 lambda 表达式不通过 -> 指定类型,则和 -> auto 等价,自动根据函数体内的 return 语句决定返回类型,如果没有 return 语句则相当于 -> void。

lambda表达式:捕获main中的变量、修改main中的变量

lambda 函数体中,还可以使用定义它的 main 函数中的变量,只需要把方括号 [] 改成 [&] 即可。
函数可以引用定义位置所有的变量,这个特性在函数式编程中称为闭包(closure)。

[&] 不仅可以读取 main 中的变量,还可以写入 main 中的变量,比如可以通过 counter++ 记录该函数被调用了多少次:

int main() {
    int fac = 2;
    int count = 0;
    auto twice = [&](int n) -> int {
        count++;
        return n * fac;
    };
    call_twice(twice);
    return 0;
}

lambda表达式:传常引用避免拷贝开销

此外,最好把模板参数的 Func 声明为 Func const & 以避免不必要的拷贝:

#include <iostream>

template <class Func>
void call_twice(Func const& func) {
    std::cout << func(0) << std::endl;
    std::cout << func(1) << std::endl;
    std::cout << "Func 的大小为:" << sizeof(func) << std::endl;
}

int main() {
    int fac = 2;
    int count = 0;
    auto twice = [&](int n) {
        count++;
        return n * fac;
    };
    call_twice(twice);
    std::cout << "调用了 " << count << " 次" << std::endl;
    return 0;
}

lambda表达式:作为返回值

既然函数可以作为参数,当然也可以作为返回值!
由于 lambda 表达式永远是个匿名类型,我们需要将 make_twice 的返回类型声明为 auto 让他自动推导。

auto make_twice() {
    return [](int n) {
        return n * 2;
    };
}

int main() {
    auto twice = make_twice();
    call_twice(twice);
    return 0;
}

作为返回值:[&]出问题了

然而当我们试图用 [&] 捕获参数 fac 时,却出了问题:
fac 似乎变成 32764 了?
这是因为 [&] 捕获的是引用,是 fac 的地址,而 make_twice 已经返回了,导致 fac 的引用变成了内存中一块已经失效的地址。
总之,如果用 [&],请保证 lambda 对象的生命周期不超过他捕获的所有引用的寿命。

auto make_twice(int fac) {
    return [&](int n) {
        return n * fac;
    };
}

int main() {
    auto twice = make_twice(2);
    call_twice(twice);
    return 0;
}

作为返回值:[=]解决问题

这时,我们可以用 [=] 来捕获,他会捕获 fac 的值而不是引用。
[=] 会给每一个引用了的变量做一份拷贝,放在 Func 类型中。
不过他会造成对引用变量的拷贝,性能可能会不如 [&]。

auto make_twice(int fac) {
    return [=](int n) {
        return n * fac;
    };
}

lambda表达式:如何避免用模板参数

虽然 这样可以让编译器对每个不同的 lambda 生成一次,有助于优化。
但是有时候我们希望通过头文件的方式分离声明和实现,或者想加快编译,这时如果再用 template class 作为参数就不行了。
为了灵活性,可以用 std::function 容器。
只需在后面尖括号里写函数的返回类型和参数列表即可,比如:std::function<int(float, char *)>;

void call_twice(std::function<int(int)> const& func) {
    std::cout << func(0) << std::endl;
    std::cout << func(1) << std::endl;
    std::cout << "Func 的大小为:" << sizeof(func) << std::endl;
}

std::function<int(int)> make_twice(int fac) {
    return [=](int n) {
        return n * fac;
    };
}

int main() {
    auto twice = make_twice(2);
    call_twice(twice);
    return 0;
}

如何避免用模板参数2:无捕获的 lambda 可以传为函数指针

另外,如果你的 lambda 没有捕获任何局部变量,也就是 [],那么不需要用 std::function<int(int)>,直接用函数指针的类型 int(int) 或者 int(*)(int) 即可。
函数指针效率更高一些,但是 [] 就没办法捕获局部变量了(全局变量还是可以的)。
最大的好处是可以伺候一些只接受函数指针的 C 语言的 API 比如 pthread 和 atexit。

void call_twice(int func(int)) {
    std::cout << func(0) << std::endl;
    std::cout << func(1) << std::endl;
    std::cout << "Func 的大小为:" << sizeof(func) << std::endl;
}

int main() {
    call_twice([](int n) {
        return n * 2;
    });
    return 0;
}

lambda + 模板:双倍快乐

可以将 lambda 表达式的参数声明为 auto,声明为 auto 的参数会自动根据调用者给的参数推导类型,基本上和 template 等价。
auto const & 也是同理,等价于模板函数的 T const &。
带 auto 参数的 lambda 表达式,和模板函数一样,同样会有惰性、多次编译的特性。

template <class Func>
void call_twice(Func const& func) {
    std::cout << func(1) << std::endl;
    std::cout << func(3.14f) << std::endl;
}

int main() {
    call_twice([](auto n) {
        return n * 2;
    });
    return 0;
}

/** 等价于:
template <class T>
 auto twice(T n) {
    return n*2;
 }
*/

C++20前瞻:函数也可以 auto,lambda 也可以

void call_twice(auto const& func) {
    std::cout << func(1) << std::endl;
    std::cout << func(3.14f) << std::endl;
}

int main() {
    call_twice([] <class T> (auto n) {
        return n * 2;
    });
    return 0;
}

lambda 用途举例:yield模式

这里用了 type_traits 来获取 x 的类型。

  • decay_t<int const &> = int
  • is_same_v<int, int> = true
  • is_same_v<float, int> = false

更多这类模板请搜索 c++ type traits。

template <class Func>
void fetch_data(Func const& func) {
    for (int i = 0; i < 32; ++i) {
        func(i);
        func(i + 0.5f);
    }
}

int main() {
    std::vector<int> res_i;
    std::vector<float> res_f;
    fetch_data([&](auto const& x) {
        using T = std::decay_t<decltype(x)>;
        if constexpr (std::is_same_v<T, int>) {
            res_i.push_back(x);
        } else if constexpr (std::is_same_v<T, float>) {
            res_f.push_back(x);
        }
    });

    return 0;
}

lambda 用途举例:立即求值

再也不需要烦人的 flag 变量

int main() {
    std::vector<int> arr = {1, 2, 3, 4, 5};
    int tofind = 3;
    int index = [&]() {
        for (int i = 0; i < arr.size(); ++i) {
            if (tofind == arr[i])
                return i;
        }
    }();
    std::cout << index << std::endl;
    return 0;
}

lambda 用途举例:局部实现递归(匿名递归)

int main() {
    std::vector<int> arr = {1, 4, 2, 8, 5, 7, 1, 4};
    std::set<int> visited;
    auto dfs = [&] (auto const& dfs, int index) -> void {
        if (visited.find(index) == visited.end()) {
            visited.insert(index);
            std::cout << index << std::endl;
            int next = arr[index];
            dfs(dfs, next);
        }
    };
    dfs(dfs, 0);
    return 0;
}

Reference:

posted @ 2022-09-20 23:46  吹不散的流云  阅读(66)  评论(0编辑  收藏  举报