博客 / 詳情

返回

基於模板的類型擦除

在C\C++中主要有三種類型擦除的方式:

  • 基於void*的類型擦除,如C標準庫的qsort函數。這中用法在C中是常見的。但因為是通過void*來操作數據,所以存在類型不安全的問題。

    • 函數原型:void qsort(void base, size_t nmemb, size_t size, int (compar)(const void , const void ))
    • 用途:對數組進行排序
    • 類型擦除:base 是一個指向數組元素的指針,其類型為 void*。這使得 qsort 可以處理任何類型的數組。
  • 面向對象的類型擦除,也就是C++中的繼承,通過父類的引用或指針來調用子類的接口。這樣解決了void*的類型不安全問題,但是繼承也帶來了代碼複雜度提升,以及侵入式設計的問題(子類的實現比如知道父類和其繼承體系)。
  • 基於模板的類型擦除,技術上來説,是編寫一個類,它提供模板的構造函數和非虛函數接口提供功能;隱藏了對象的具體類型,但保留其行為。典型的就是std::function。

下面是一個示例代碼,實現了通用的任務,這些任務可以是任意的函數對象。

#include <iostream>
#include <memory>

// 抽象基類TaskBase作為公共接口不變;其子類TaskModel寫成類模板的形式,其把一個任意類型F的函數對象function_作為數據成員。
struct TaskBase
{
    virtual ~TaskBase() {}
    virtual void operator()() const = 0;
};

template <typename F>
struct TaskModel : public TaskBase
{
    F functor_;

    template <typename U> // 構造函數是函數模板
    TaskModel(U &&f) : functor_(std::forward<U>(f))
    {
    }

    void operator()() const override
    {
        functor_();
    }
};

// 對TaskModel的封裝
class MyTask
{
    std::unique_ptr<TaskBase> ptr_;

public:
    template <typename F>
    MyTask(F &&f)
    {
        using ModelType = TaskModel<F>;
        ptr_ = std::make_unique<ModelType>(std::forward<F>(f));
    }

    void operator()() const
    {
        ptr_->operator()();
    }
};

/////////////測試代碼/////////////////

// 普通函數
void func1()
{
    std::cout << "type erasure 1" << std::endl;
}

// 重載括號運算符的類
struct func2
{
    void operator()() const
    {
        std::cout << "type erasure 2" << std::endl;
    }
};
int main()
{
    // 普通函數
    MyTask t1{&func1};
    t1(); // 輸出"type erasure 1"

    // 重載括號運算符的類
    MyTask t2{func2{}};
    t2(); // 輸出"type erasure 2"

    // Lambda
    MyTask t3{
        []()
        { std::cout << "type erasure 3" << std::endl; }};
    t3(); // 輸出"type erasure 3"

    return 0;
}

總結下,要實現基於模板的類型擦除主要有三層的代碼。

  • 第一層是concept,TaskBase。考慮需要的功能後,以虛函數的形式提供對應的接口I。
  • 第二層是model,TaskModel。這是一個類模板,用來存放用户提供的類T,T應當語法上滿足接口I;重寫concept的虛函數,在虛函數中調用T對應的函數。
  • 第三層是wrapper,對應MyTask。存放一個concept指針p指向model對象m;擁有一個模板構造函數,以適應任意的用户提供類型;以非虛函數的形式提供接口I,通過p調用m。

從技術上來説,這種類型擦除的技巧可算是運行時多態的一種另類實現。它避免了一個類通過繼承這種帶來類間強耦合關係的方式,去滿足某個運行時多態使用(polymorphic use)的需求。

測試代碼表明,用户可以把任意的滿足void()簽名接口的函數對象作為任務,但不需要手動繼承任何的代碼或編寫虛函數。實現任務類的運行時多態的代碼被限制在庫的範圍內,不會以繼承的方式侵入用户的代碼或其他的庫。

這裏還有另一個示例。

首先,定義圖形的概念接口ShapeConcept,接口類中定義了接口函數Draw。然後,通過模板ShapeModel具體實現了ShapeConcept的概念。最後,定義Shape類來封裝ShapeModel。

這樣,定義了Draw接口的正方形Square或者是通過特化實現了Draw的圓形Circle,都可以統一到Shape下,而不需要繼承它。

#include <iostream>
#include <memory>
#include <utility>
#include <vector>

// 圖形的概念接口
struct ShapeConcept
{
    virtual void Draw() const = 0;
};

// 圖形概念的具體實現
template <typename T>
struct ShapeModel : ShapeConcept
{
    ShapeModel(T &&val) : shape_{std::forward<T>(val)} {}
    void Draw() const override
    {
        shape_.Draw(); // 這裏假設具體圖形有Draw()成員函數。如果沒有,需要特化該模板。
    }

private:
    // 這裏直接存儲具體圖形的值
    T shape_;
};

// 父類
class Shape
{
public:
    template <typename T>
    Shape(T &&val) : pimpl_{new ShapeModel<T>(std::forward<T>(val))} {}
    inline void Draw() const
    {
        pimpl_->Draw();
    }

private:
    std::unique_ptr<ShapeConcept> pimpl_;
};

//---------------------正方形-------------------------
class Square
{
public:
    explicit Square(float side) : side_(side) {}
    // 正方形的繪圖函數是一個成員函數
    void Draw() const
    {
        std::cout << "Draw square of side: " << side_ << std::endl;
    }

private:
    float side_;
};

//---------------------圓-----------------------------
class Circle
{
public:
    explicit Circle(float radius) : radius_(radius) {}
    float GetRadius() const { return radius_; }

private:
    float radius_;
};
// 圓的繪圖是一個全局函數
void DrawCircle(const Circle &circle)
{
    std::cout << "Draw circle of radius: " << circle.GetRadius() << std::endl;
}
// 因為圓的繪圖函數是一個全局函數,所以需要特化
template <>
struct ShapeModel<Circle> : ShapeConcept
{
    ShapeModel(Circle &&val) : shape_(std::move(val)) {}
    void Draw() const override
    {
        DrawCircle(shape_);
    }

private:
    Circle shape_;
};

int main()
{
    std::vector<Shape> shapes;
    shapes.push_back(Circle(1.0));
    shapes.push_back(Square(2.0));
    for (const auto &shape : shapes)
    {
        shape.Draw();
    }
    return 0;
}
user avatar luguolangren 頭像 tulingxiaobian 頭像 mrbone11 頭像 yunwei37 頭像
4 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.