函数式设计模式

函数指针

首先需要定义函数指针类型:

using func_t = int (*)(int i);
// 等价于
typedef int (*func_t)(int i);

函数指针可以用于实现策略模式:

void test(func_t func) {
    cout << func(2);
    cout << func(3);
}

int twice(int i) {
    return i * 2;
}

int main() {
    test(twice); // 4 6
}

函数指针实现多态

int twice(int i) {
    return i * 2;
}

int triple(int i) {
    return i * 3;
}

int main() {
    test(twice);  // 4 6
    test(triple); // 6 9
}

函数指针的缺陷

缺点:函数指针只能指向全局函数,无法保存状态。

int ntimes(int scale, int i) {
    return i * scale;
}

int main() {
    test(ntimes(2)); // 错误:没有这种语法
    test(ntimes(3)); // 错误:没有这种语法
}

函数调用运算符

struct myclass {
    void run(string name) {
        cout << name << ",你好\n";
    }
};

myclass mc;
mc.run("小切");
struct myclass {
    void operator() (string name) {
        cout << name << ",你好\n";
    }
};

myclass mc;
mc. operator() ("小切");
struct myclass {
    void operator() (string name) {
        cout << name << ",你好\n";
    }
};

myclass mc;
mc("小切");

重载了 operator() 后的 myclass 对象,调用起来就和普通函数一样。

仿函数

仿函数是一种重载了函数调用运算符的类,可以像函数一样使用:

struct twice_t {
    int operator() (int i) {
        return i * 2;
    }
};

void test(twice_t func) {
    cout << func(2);
    cout << func(3);
}

twice_t twice;

int main() {
    test(twice); // 4 6
}

仿函数的优势是可以保存状态

struct ntimes_t {
    int scale;

    ntimes_t(int scale) : scale(scale) {}

    int operator() (int i) {
        return i * scale;
    }
};

void test(ntimes_t func) {
    cout << func(2);
    cout << func(3);
}

int main() {
    ntimes_t twice(2);
    ntimes_t triple(3);
    ntimes_t quadric(4);
    test(twice);   // 4 6
    test(triple);  // 6 9
    test(quadric); // 8 12
}

仿函数的缺陷

缺点:需要在 test 中写明仿函数的具体类型,无法实现多态。

struct twice_t {
    int operator() (int i) {
        return i * 2;
    }
};

struct triple_t {
    int operator() (int i) {
        return i * 2;
    }
};

void test(twice_t func) {
    cout << func(2);
    cout << func(3);
}

twice_t twice;
triple_t triple;

int main() {
    test(twice);  // 4 6
    test(triple); // 错误:test 只兼容了 twice_t 做参数
}

如何解决?

利用模板

template <class Func>
void test(Func func) {
    cout << func(2);
    cout << func(3);
}

twice_t twice;
triple_t triple;

int main() {
    test(twice);  // 4 6
    test(triple); // 6 9
}

这样就实现了编译期多态,因为 test 函数的参数类型可以根据传入的具体函数类型进行推导。

模板传仿函数的缺陷

缺点:必须编译期确定,无法动态决定类型。

template <class Func>
void test(Func func) {
    cout << func(2);
    cout << func(3);
}

twice_t twice;
triple_t triple;

int main() {
    bool ok;
    cin >> ok;
    test(ok ? twice : triple); // 错误:不兼容的类型之间不能三目
}

万能的 function 容器

std::function 采用了类型擦除技术,无需写明仿函数类的具体类型,能容纳任何仿函数或函数指针。

只需在模板参数中写明函数的参数和返回值类型即可,所有具有同样参数和返回值类型的仿函数或函数指针都可以传入。

struct twice_t {
    int operator() (int i) {
        return i * 2;
    }
};

function<int (int)> twice = twice_t();  // 没问题,能接受仿函数


int triple(int i) {
    return i * 3;
}

function<int (int)> function_triple = triple; // 没问题,能接受函数指针


struct ntimes_t {
    int scale;

    ntimes_t(int scale) : scale(scale) {}

    int operator() (int i) {
        return i * scale;
    }
};

function<int (int)> quadric = ntimes_t(4); // 没问题,能接受带状态的仿函数

可以用 function 容器作为参数,就可以避免使用模板。

// 模板:性能优先
template <class Func>
void test(Func func) {
    cout << func(2);
    cout << func(3);
}

// 容器:灵活性优先
void test(function<int (int)> func) {
    cout << func(2);
    cout << func(3);
}

函数式为什么好?

有人说,function 底层依然是基于函数指针实现的,不是和虚函数一样低效吗?函数式相比传统面向对象好在哪里呢?

  1. 性能与灵活性的选择权

现实工程中,往往是 20% 的代码耗费了 80% 的计算机时间。我们只要优化这 20% 的瓶颈代码就可以。

虚函数实现的多态,是强制的,一旦用了虚函数,就没法把虚表去掉了,永远卡在你的对象类型里占 8 字节空间,永远只能以指针形态使用。

如果选择函数式编程范式,你可以在次要的业务逻辑代码中选择更灵活的 function 容器。

而在需要性能的瓶颈代码处,可以随时切换到基于模板的,更高性能的编译期多态。函数式给了你根据情况选择的自由度。

  1. function 有小对象优化

而且 function 内部具有类似于 string 小对象优化机制,对于较小的状态或没有状态的仿函数,就无需指针!无需堆内存分配!

而虚函数哪怕没有状态,由于虚表指针的存在,也总是需要用 new 创建,总是会造成大量的碎片化内存,因此即使同样选择了灵活性,function 依然比虚函数高效一点。

而且即使你的状态非常多,导致 function 不得不需要堆内存分配了,这一分配也不用你自己操心,不用手动 new,也不用手动 make_shared,function 内部自动帮你完成一切。

  1. lambda 表达式很方便

最重要的是,函数式编程范式可以便捷地利用 lambda 表达式就地创建仿函数对象,而面向对象需要大费周章定义一个类接口,然后再定义一个类实现虚函数,有时还需要分离声明和定义。

绕了个大圈子,不仅写起来痛苦,需要起名强迫症,而且看得人也头疼。lambda 表达式,就地创建,无需名字,更适合敏捷开发。