文章目錄
- 一、C++類型擦除Type Erasure技術
- 1.虛函數
- 2.模板和函數對象
- 二、任務隊列
- 1.基于特定類型的方式實現
- 2.基于任意類型的方式實現
- 參考:
一、C++類型擦除Type Erasure技術
C++中的類型擦除(Type Erasure)是一種技術,用于隱藏具體類型并以類型無關的方式處理對象。 它允許在運行時處理不同類型的對象,同時提供一致的接口和行為。
類型擦除常用于實現泛型編程和多態性,其中需要處理不同類型的對象,但又希望以一致的方式進行操作和處理。
兩個常見的類型擦除技術:虛函數,模板和函數對象
1.虛函數
- 使用虛函數是一種簡單的類型擦除技術,通過將函數聲明為虛函數,可以在派生類中重寫該函數以提供具體實現。
- 然后,可以使用基類指針或引用來處理不同派生類的對象,而無需關心具體的類型。 虛函數機制提供了動態派發的能力,使得在運行時選擇正確的函數實現。
派生類是普通類
class Base {
public:virtual void foo() {// 基類默認實現}
};class Derived1 : public Base {
public:void foo() override {// 派生類1的實現}
};class Derived2 : public Base {
public:void foo() override {// 派生類2的實現}
};void process(Base& obj) {obj.foo(); // 調用適當的派生類實現
}int main() {Derived1 d1;Derived2 d2;process(d1); // 調用Derived1的foo()process(d2); // 調用Derived2的foo()return 0;
}
在上述示例中,
- Base類具有虛函數 foo(),并且它的派生類 Derived1 和 Derived2 分別提供了自己的實現。
- process() 函數接受 Base 類型的引用,可以在運行時根據實際的對象類型來調用適當的 foo() 實現。
派生類是模板類
#include <iostream>struct Base {virtual void foo() const = 0;
};template <typename T>
struct Derived : public Base {void foo() const override {std::cout << "Derived<" << typeid(T).name() << ">::foo()" << std::endl;}
};void process(const Base& obj) {obj.foo(); // 調用適當的派生類實現
}int main() {Derived<int> d1;Derived<double> d2;process(d1); // 調用Derived<int>::foo()process(d2); // 調用Derived<double>::foo()return 0;
}
在上述示例中,Base 是一個抽象基類,其派生類 Derived 是一個模板類。
- 模板參數 T 表示派生類的具體類型。通過在 Derived 類中使用 typeid 和 name(),可以在運行時獲取具體類型的信息。
- process() 函數接受 Base 類型的常量引用,并調用適當的 foo() 實現。
2.模板和函數對象
另一種類型擦除的方法是使用模板和函數對象(Functor)。通過使用模板和函數對象,可以將類型信息推遲到運行時,并以一致的方式使用對象。
使用仿函數
- 這個例子展示了如何使用函數對象實現類型擦除,通過函數對象的模板化操作符 operator(),我們可以在運行時以一致的方式處理不同類型的對象。
#include <iostream>
#include <typeinfo>struct Base {virtual void foo() const = 0;
};struct Functor {template <typename T>void operator()(const T& obj) const {std::cout << "Functor: " << typeid(T).name() << std::endl;obj.foo();}
};template <typename T>
struct Derived : public Base {void foo() const override {std::cout << "Derived<" << typeid(T).name() << ">::foo()" << std::endl;}
};int main() {Derived<int> d1;Derived<double> d2;Functor f;f(d1); // 調用Derived<int>::foo()f(d2); // 調用Derived<double>::foo()return 0;
}
-
我們定義了一個函數對象 Functor,其中的 operator() 是一個模板函數。函數對象通過調用 obj.foo() 來執行對象的 foo() 方法,并在控制臺上打印相關信息。Derived 類是一個模板類,它繼承自 Base 類,實現了 foo() 方法。
-
在 main() 函數中,我們創建了兩個不同類型的 Derived 對象 d1 和 d2,然后創建了一個 Functor 對象 f。通過調用 f(d1) 和 f(d2),我們將不同類型的對象傳遞給函數對象 f,它將根據對象的類型調用適當的 foo() 實現。
使用std::function
- 通過使用 std::function,我們可以將不同類型的可調用對象進行類型擦除,并以一致的方式進行處理。
#include <iostream>
#include <functional>struct Base {virtual void foo() const = 0;
};struct Derived1 : public Base {void foo() const override {std::cout << "Derived1::foo()" << std::endl;}
};struct Derived2 : public Base {void foo() const override {std::cout << "Derived2::foo()" << std::endl;}
};void process(const std::function<void()>& func) {func(); // 調用適當的函數實現
}int main() {Derived1 d1;Derived2 d2;std::function<void()> func1 = [&d1]() { d1.foo(); };std::function<void()> func2 = [&d2]() { d2.foo(); };process(func1); // 調用Derived1::foo()process(func2); // 調用Derived2::foo()return 0;
}
-
在這個示例中,我們定義了 Base 類和兩個派生類 Derived1 和 Derived2。Base 類有一個純虛函數 foo(),每個派生類都提供了自己的實現。
-
process() 函數接受一個 std::function<void()> 類型的參數,它表示一個無返回值、不帶參數的可調用對象。通過傳遞不同的 std::function 對象給 process() 函數,我們可以在運行時選擇適當的函數實現。
進一步,使用std::bind
- 使用 std::bind 可以實現類型擦除和延遲綁定,允許在運行時選擇函數實現,并提供具體的參數值。
#include <iostream>
#include <functional>struct Base {virtual void foo(int value) const = 0;
};struct Derived1 : public Base {void foo(int value) const override {std::cout << "Derived1::foo(" << value << ")" << std::endl;}
};struct Derived2 : public Base {void foo(int value) const override {std::cout << "Derived2::foo(" << value << ")" << std::endl;}
};void process(const std::function<void()>& func) {func(); // 調用適當的函數實現
}int main() {Derived1 d1;Derived2 d2;auto func1 = std::bind(&Derived1::foo, &d1, 42);auto func2 = std::bind(&Derived2::foo, &d2, 24);process(func1); // 調用Derived1::foo(42)process(func2); // 調用Derived2::foo(24)return 0;
}
- 在示例中,func1 綁定了 Derived1::foo 成員函數,并提供了一個參數值 42。同樣地,func2 綁定了 Derived2::foo 成員函數,并提供了一個參數值 24。通過調用 process() 函數,我們可以分別調用適當的函數實現。
二、任務隊列
1.基于特定類型的方式實現
假設任務類如下所示:
//任務隊列的類型是my_queue<std::unique_ptr<task_base>>,用基類指針去管理任務對象
class my_thread {using task_type = void(*)();my_queue<std::unique_ptr<task_base>> task_queue;//處理不同子類對象的run()的邏輯,可能實現void Loop() noexcept{for(auto& task: task_queue){task->run();}}
}; // 假設具體的任務函數體的調用簽名都是void
struct task_base {virtual ~task_base() = 0;virtual void run() const = 0;
};// 用戶編寫的具體任務類
struct task_impl : public task_base { void run() const override {// 運算...}
};
優點:容易實現
缺點:非常缺乏伸縮性。
- 首先,編寫子類的責任被推給了用戶,可能一個不太復雜的函數調用會被強加上任務基類task_base的包裝;
- 而且用起來也不方便。
2.基于任意類型的方式實現
使用類型擦除技術,這類設施典型的代表就是std::function,它通過類型擦除的技巧,不必麻煩用戶編寫繼承相關代碼,并能包裝任意的函數對象。
C++語境下的類型擦除,技術上來說,是編寫一個類,它提供模板的構造函數和非虛函數接口提供功能;隱藏了對象的具體類型,但保留其行為。
- 簡單地說,就是庫作者把面向對象的代碼寫了,而不是推給用戶寫:
- 首先,抽象基類task_base作為公共接口不變;
- 其子類task_model(角色同上文中的task_impl)寫成類模板的形式,其把一個任意類型F的函數對象function_作為數據成員。
- 子類寫成類模板的具體用意是,對于用戶提供的一個任意的類型F,F不需要知道task_base及其繼承體系,而只進行語法上的duck typing檢查。 這種方法避免了繼承帶來的侵入式設計。 換句話說,只要能合乎語法地對F調用預先定義的接口,代碼就可以編譯,這個技巧就能運作。
- 此例中,預先定義的接口是void(),以functor_();的形式調用。
struct task_base {virtual ~task_base() {}virtual void operator()() const = 0;
};template <typename F>
struct task_model : public task_base {F functor_;template <typename U> // 構造函數是函數模板task_model(U&& f) :functor_(std::forward<U>(f)) {}void operator()() const override {functor_();}
};
然后,我們把它包裝起來:
- 首先,初始動機是用一個類型包裝不同的函數對象。
- 然后,考慮這些函數對象需要提供的功能(affordance),此處為使用括號運算符進行函數調用。
- 最后,把這個功能抽取為一個接口,此處為my_task,我們在在這一步擦除了對象具體的類型。
- 這便是類型擦除的本質:切割類型與其行為,使得不同的類型能用同一個接口提供功能。
class my_task {std::unique_ptr<task_base> ptr_;public:template <typename F>my_task(F&& f) {using model_type = task_model<F>;ptr_ = std::make_unique<model_type>(std::forward<F>(f)); }void operator()() const {ptr_->operator()();} // 移動構造函數my_task(my_task&& oth) noexcept : ptr_(std::move(oth.ptr_)){}// 移動賦值函數my_task& operator=(my_task&& rhs) noexcept {ptr_ = std::move(rhs.ptr_);return *this;}
};class my_thread {using task_type = void(*)();my_queue<my_task> task_queue;//處理不同子類對象的run()的邏輯,可能實現void Loop() noexcept{for(auto& task: task_queue){task();}}
};
測試:
對my_task進行簡單測試的代碼如下:
- 其實完全可以用std::function代替my_task,來實現類型擦除,這樣連虛函數都不需要了;如果采用虛函數的方式,可以參考1和2的方法去設計
// 普通函數
void foo() {std::cout << "type erasure 1";
}
my_task t1{ &foo };
t1(); // 輸出"type erasure 1"// 重載括號運算符的類
struct foo2 {void operator()() {std::cout << "type erasure 2";}
};
my_task t2{ foo2{} };
t2(); // 輸出"type erasure 2"// Lambda
my_task t3{[](){ std::cout << "type erasure 3"; }
};
t3(); // 輸出"type erasure 3"
總結:
- 第一層是task_base。考慮需要的功能后,以虛函數的形式提供對應的接口I。
- 第二層是task_model。這是一個類模板,用來存放用戶提供的類T,T應當語法上滿足接口I;重寫task_base的虛函數,在虛函數中調用T對應的函數。
- 第三層是對應my_task。存放一個task_base指針p指向task_model對象m;擁有一個模板構造函數,以適應任意的用戶提供類型;以非虛函數的形式提供接口I,通過p調用m。
上述可能存在的問題1:
my_task t1{ &foo1 };/*
foo作為參數傳遞給一個函數模板時,會被“準確”地推斷為函數類型void(),而不是函數指針類型void(*)(
*/
my_task t2{ foo1 }; // 編譯出錯,
-
解決辦法1:簡單的解決方法是,每次都記得用取地址運算符&
-
解決辦法2:讓模板將函數類型推導為函數指針類型void(*)(),修改my_task的構造函數為:
class my_task {template <typename F>my_task(F&& f) {// 使用std::decay來顯式地進行類型退化// 如果傳入函數類型就退化為函數指針類型using F_decay = std::decay_t<F>;using model_type = task_model<F_decay>; ptr_ = std::make_unique<model_type>(std::forward<F_decay>(f));}
};
模板元編程就是在編譯時進行運算并生成代碼的代碼(所謂“元”)
上述可能存在的問題2:
// 復制構造my_task
my_task t1{[]() { std::cout << "type erasure"; }
};/*
事實上,如果這樣去構造t2,編譯器不會報錯,但是運行時會棧溢出!如果查看棧記錄,會發現程序一直在my_task的構造函數和task_model構造函數之間無限循環。Word?
*/
my_task t2{ t1 }; // 發生了什么?
從編譯期函數解析的角度,分析這段代碼可以通過編譯的原因:
從t1復制構造t2時,編譯器的第一選擇是my_task的復制構造函數,但它被禁用了;
于是,編譯器退而求其次地嘗試匹配my_task的第一個構造函數,template my_task(F&&)。
而這個構造函數并沒有限制F不能為my_task, 編譯器就選擇調用它。所以,這段代碼可以過編譯。
解決辦法:
- 禁止my_task的模板構造函數的類型參數F為my_task
template <typename F>
using is_not_my_task = std::enable_if_t<!std::is_same_v< std::remove_cvref_t<F>, my_task >,int>;template <typename F, is_not_my_task<F> = 0>
my_task(F&& f);使用C++20 Concept
template <typename F>
concept is_not_my_task = !std::is_same_v<std::remove_cvref_t<F>, my_task>;class my_task {template <typename F> requires is_not_my_task<F>my_task(F&& f);
};
參考:
- 深入淺出C++類型擦除(1)
- 深入淺出C++類型擦除(2)