簡介 C++ 的 Type Erase (用多型和模板做 Duck Type)

·

3 min read

起點

讓我們先從 template 出發:foo 需要一個 callback function。

template<typename Func>
void foo(Func callback) {
    // ...
    callback();
}

但是這會讓編譯錯誤訊息有點模糊:假如 callback 並不是一個可以呼叫的函數指標,或者並不是一個 callable object ,那編譯器會說錯出在第四行。但是我們都希望,編譯器在呼叫函數時就幫我們指出:這不是 foo 想要的 callback 參數。

STL 裡面就有提供 std::function<void()> 可以存下 foo 所需要的 callback,這就會倒出一個問題:我們要怎麼做出 std::function<void()>

以下先忽略 private 包裝和 move,這些不是這文章的重點。

用 class 包裝?

我們先捏一個 struct:

template <typename Func>
struct Callable {
    Func func_;
    explicit Callable(Func func) : func_(func) { }

    void operator()() {
        func_();
    }
};

可以用 Callable 包起來的 Func ,肯定可以被我們用 func_() 的方式呼叫,可是這仍然沒辦法當作一個完整 Type – 我們仍然缺一個 template parameter。

多型?

但我們可以在上面再加蓋一層:

struct CallableType {
    CallableType() = default;
    virtual ~CallableType() = default;
    virtual void operator()() = 0;
};

template <typename Func>
struct Callable : public CallableType {
    Func func_;
    Callable(Func func) : func_(func) { }
    void operator()() {
        func_();
    }
};

要使用 CallableType 的話仍然需要一層指標讓多型 work,這樣用起來很麻煩。所以我們再包一層:CallableObj,並且讓型別在 constructor 就自動辨別好。

struct CallableObj {
    std::unique_ptr<CallableType> callableType_;

    template <typename Func>
    CallableObj(Func func)
        : callableType_(std::make_unique<Callable<Func>>(func)) { }

    void operator()() {
        (*callableType_)();
    }
};

這時,我們便可以用 CallableObj 當作 foo 的參數了! … 嗎???

void foo(Func callback) {
    // ...
    callback();
}

Copy

因為 CallableObj 裡面有一個 std::unique_ptr<CallableType>,所以 CallableObj 是複製不了的。

function "CallableObj::CallableObj(const CallableObj &)"
(declared implicitly) cannot be referenced -- it is a deleted function

我們需要繼承 CallableType 的 Callable<Func> 提供一個 clone() 的介面,讓我們可以好好的複製被 CallableType 藏起來的東西,也就是 Func。

struct CallableType {
    // ...
    virtual std::unique_ptr<CallableType> clone() const = 0;
};

template <typename Func>
struct Callable : public CallableType {
    // ...
    // just copy
    std::unique_ptr<CallableType> clone() const override {
        return std::make_unique<Callable<Func>>(*this);
    }
};

所以我們就可以把 CallableObj 的 copy constructor/assignment 補上

CallableObj(const CallableObj& other):
    callableType_(other.callableType_->clone()) { }

CallableObj& operator=(const CallableObj& other) {
    CallableObj tmp(other);
    callableType_.swap(tmp.callableType_);
    return *this;
}

Code

最後,再補上 move constructor/assignment 和 null pointer check。

Code:

#include <iostream>
#include <memory>

struct CallableType {
    CallableType() = default;
    virtual ~CallableType() = default;
    virtual void operator()() = 0;
    virtual std::unique_ptr<CallableType> clone() const = 0;
};

template <typename Func>
struct Callable : public CallableType {
    Func func_;
    explicit Callable(Func func) : func_(func) { }

    void operator()() {
        func_();
    }

    std::unique_ptr<CallableType> clone() const override {
        return std::make_unique<Callable<Func>>(*this);
    }
};

struct CallableObj {
    std::unique_ptr<CallableType> callableType_;

    template <typename Func>
    CallableObj(Func func)
        : callableType_(std::make_unique<Callable<Func>>(func)) { }

    ~CallableObj() = default;
    CallableObj(CallableObj&&) = default;
    CallableObj& operator=(CallableObj&&) = default;

    CallableObj(const CallableObj& other) {
        if (other.callableType_) {
            callableType_ = other.callableType_->clone();
        }
    }

    CallableObj& operator=(const CallableObj& other) {
        CallableObj tmp(other);
        callableType_.swap(tmp.callableType_);
        return *this;
    }

    void operator()() {
        if (callableType_) {
            (*callableType_)();
        }
    }
};

void foo(CallableObj callback) {
    callback();
}

int main() {
    CallableObj co([]() { std::cout << "hello\n"; });
    foo(co);              // output hello
    foo(std::move(co));   // output hello
    foo(co);              // no output
    return 0;
}

More...?

所以,還有那些沒做到?

  1. Private 包裝:func_ 和 callableType_ 應該要是 private member

  2. Func 要考慮 move semantic

  3. 要怎麼用 Template 做到 CallableObj<RetType(ArgType1, ArgType2, ...)>