类型擦除

题目描述

众所周知,Lambda 表达式的类型名是无法获取的,每个 Lambda 都拥有独一无二的类型。因此我们处理 Lambda 表达式时大多使用模板。但是在很多场合模板并不适用,我们希望能有一个通用的“类型”来接纳所有的、可以调用的函数对象。

这就需要用到类型擦除技术。类型擦除主要依赖于多态和模板两个语法。

接下来我们将编写一个 Function 类。它可以用任何类型的对象初始化,只要它可以被无参地调用。这样初始化后,该 Function 对象就 包装 了原始的可调用对象。同时,Function 类也实现 operator(),从而调用其内部被包装的真正的可调用对象。

// 下面两行的 Lambda 表达式类型不同,但都可以包装到 Function 内部
Function f1 = [=] () { std::printf("f1"); };
Function f2 = [=] () { std::printf("f2"); };

那么如何实现 Function 呢?首先很显然,它有一个构造函数模板,以接受来自任意类型对象的初始化。此外,Function 中有一个 Callable* 类型的指针(稍后展开)。在上述构造函数中,若形参名为 f,具有 F 类型;那么该指针将用 new FuncWrapper<F>(f) 初始化。

虽然我们还不知道 CallableFuncWrapper<F> 是什么;但是从上文的描述来看,可以知道它们的指针之间能够转换。是的,可以猜到 FuncWrapper<F>Callable 的派生类。不仅如此,它们还是多态的。

Callable 基类是抽象类。它有一个接口 void call();FuncWrapper 是类模板:它存储一份 F 类型的对象成员 F f;。它在实现 call 接口时,调用成员 f

Functionoperator() 中,调用基类 Callablecall 接口。由于这个接口是多态的,所以它会调用到 FuncWrapper<F> 的实现上;而后者就直接调用 F 类型对象的 () 运算符。这就实现了用非模板、非泛型的 Function 类,包装任意类型的可调用对象。

请根据上述描述,实现 Function

关于输入

关于输出

见样例输出

参考答案

#include <iostream>

class Callable {
public:
    virtual void call() = 0;
    virtual ~Callable() = default;
};

template <typename T>
class FuncWrapper : public Callable {
    T f;

public:
    FuncWrapper(T f) : f(f) {}
    void call() override {
        f();
    }
};

class Function {
    Callable* p;

public:
    Function() : p(nullptr) {}
    template <typename T>
    Function(T f) : p(new FuncWrapper<T>(f)) {}
    void operator()() {
        p->call();
    }
    ~Function() {
        delete p;
    }
};

void f1() {
    std::cout << "f1" << std::endl;
}
struct F2 {
    void operator()() {
        std::cout << "f2" << std::endl;
    }
};
F2 f2;
int main() {
    int v = 3;
    auto f3 = [&]() {
        std::cout << "f" << v << std::endl;
    };
    
    Function funcs[]{f1, f2, f3};

    for (int i = 0; i < 3; i++) {
        funcs[i]();
    }
}