C++中的類型擦除技術

文章目錄

  • 一、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)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/42622.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/42622.shtml
英文地址,請注明出處:http://en.pswp.cn/news/42622.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Electron基礎篇

人生有些事,錯過一時,就錯過一世。 官網&#xff1a;簡介 | Electron Electron-大多用來寫桌面端軟件 Electron介紹 Electront的核心組成是Chromium、Node.js以及內置的Native API&#xff0c;其中Chromium為Electron提供強大的UI能力&#xff0c;可以在不考慮兼容的情況下利…

使用神卓互聯內網穿透搭建遠程訪問公司ERP系統

神卓互聯是一款企業級內網穿透軟件&#xff0c;可以將內網中的服務映射到公網上&#xff0c;實現內網服務的訪問。通過神卓互聯&#xff0c;您可以遠程訪問ERP系統。在使用神卓互聯進行內網穿透時&#xff0c;您只需要在生成的公網地址后面加上ERP系統的端口號&#xff0c;即可…

NVIDIA vGPU License許可服務器高可用全套部署秘籍

第1章 前言 近期遇到比較多的場景使用vGPU&#xff0c;比如Citrix 3D場景、Horizon 3D場景&#xff0c;還有AI等&#xff0c;都需要使用顯卡設計研發等&#xff0c;此時許可服務器尤為重要&#xff0c;許可斷掉會出現掉幀等情況&#xff0c;我們此次教大家部署HA許可服務器。 …

【.net】本地調試運行只能用localhost的問題

【.net】本地調試運行只能用localhost的問題 解決方案 找到到項目目錄下 隱藏文件夾 .vs /項目名稱/config/applicationhost.config <bindings><binding protocol"http" bindingInformation"*:1738:localhost" /></bindings> 再加一條你…

職業學院物聯網實訓室建設方案

一、概述 1.1專業背景 物聯網&#xff08;Internet of Things&#xff09;被稱為繼計算機、互聯網之后世界信息產業第三次浪潮&#xff0c;它并非一個全新的技術領域&#xff0c;而是現代信息技術發展到一定階段后出現的一種聚合性應用與技術提升&#xff0c;是隨著傳感網、通…

如何判斷自己是否適合游戲開發?

引言 游戲開發是一個充滿創意和技術挑戰的領域&#xff0c;吸引著越來越多的年輕人投身其中。然而&#xff0c;要想在游戲開發領域獲得成功&#xff0c;首先需要明確自己是否適合這個領域。本文將為你介紹一些判斷自己是否適合游戲開發的關鍵因素。 1. 技術興趣和編程能力 游…

Python 程序設計入門(024)—— Python 的文件操作

Python 程序設計入門&#xff08;024&#xff09;—— Python 的文件操作 目錄 Python 程序設計入門&#xff08;024&#xff09;—— Python 的文件操作一、文件對象二、讀取文件內容的方法1、read() 方法2、readline() 方法3、readlines() 方法4、使用 for 循環讀取文件內容 …

麥肯錫發布《2023科技趨勢展望報告》,生成式AI、下一代軟件開發成為趨勢,軟件測試如何貼合趨勢?

近日&#xff0c;麥肯錫公司發布了《2023科技趨勢展望報告》。報告列出了15個趨勢&#xff0c;并把他們分為5大類&#xff0c;人工智能革命、構建數字未來、計算和連接的前沿、尖端工程技術和可持續發展。 類別一&#xff1a;人工智能革命 生成式AI 生成型人工智能標志著人工智…

CSRF

文章目錄 CSRF(get)CSRF(post)CSRF Token CSRF(get) 根據提示的用戶信息登錄 點擊修改個人信息 開啟bp代理&#xff0c;點擊submit 攔截到請求數據包 瀏覽器關閉代理 刷新頁面 CSRF(post) 使用BP生成CSRF POC post請求偽造&#xff0c;可以通過釣魚網站&#xff0c;誘導用戶去…

docker 常用命令大全

1.查看docker版本&#xff1a; docker -v2.檢查 Docker 是否正在運行: systemctl status docker3.重啟docker服務: systemctl restart docker4.列出本地鏡像: docker images5.列出正在運行的容器&#xff1a; docker ps6.列出所有容器&#xff08;包括停止的&#xff09;&…

css 實現文字橫向循環滾動

實現效果 思路 ## 直接上代碼,html部分 //我這里是用的uniapp <view class"weather_info_wrap"><view class"weather_info">當前多云&#xff0c;今晚8點轉晴&#xff0c;明天有雨&#xff0c;溫度32攝氏度。</view><view class&qu…

CF1005A Tanya and Stairways 題解

題目傳送門 題目意思&#xff1a; 給你 n n n 個數&#xff0c;如果第 i i i 個數小于或等于第 i ? 1 i-1 i?1 個數&#xff0c;就輸出這個數。 思路&#xff1a; 輸入后直接遍歷判斷即可。 代碼&#xff1a; #include<bits/stdc.h> using namespace std; int …

解決IDEA tomcat控制臺只有server日志

解決IDEA tomcat控制臺只有server日志 確認tomcatxxx/conf/logging.properties文件是否存在&#xff0c;存在就會有。前提是在run configuration配置了打印多個日志

uniapp封裝組件,選中后右上角顯示對號√樣式(通過css實現)

效果&#xff1a; 一、組件封裝 1、在項目根目錄下創建components文件夾&#xff0c;自定義組件名稱&#xff0c;我定義的是xc-button 2、封裝組件代碼 <template><view class"handle-btn"><view :class"handleIdCode 1 ? select : unSelec…

螞蟻數科持續發力PaaS領域,SOFAStack布局全棧軟件供應鏈安全產品

8月18日&#xff0c;記者了解到&#xff0c;螞蟻數科再度加碼云原生PaaS領域&#xff0c;SOFAStack率先完成全棧軟件供應鏈安全產品及解決方案的布局&#xff0c;包括靜態代碼掃描Pinpoint、軟件成分分析SCA、交互式安全測試IAST、運行時防護RASP、安全洞察Appinsight等&#x…

【電商領域】Axure在線購物商城小程序原型圖,品牌自營垂直電商APP原型

作品概況 頁面數量&#xff1a;共 60 頁 兼容軟件&#xff1a;Axure RP 9/10&#xff0c;不支持低版本 應用領域&#xff1a;網上商城、品牌自營商城、商城模塊插件 作品申明&#xff1a;頁面內容僅用于功能演示&#xff0c;無實際功能 作品特色 本作品為品牌自營網上商城…

無涯教程-Perl - warn函數

描述 此函數將LIST的值打印到STDERR。基本上與die函數相同,除了不對出口進行任何調用并且在eval語句內不引發異常。這對于引發錯誤而不導致腳本過早終止很有用。 如果變量$包含一個值(來自先前的eval調用),并且LIST為空,則$的值將以。\t.caught打印。附加到末尾。如果$和LIST…

MySQL數據庫概述

MySQL數據庫概述 1 SQL SQL語句大小寫不敏感。 SQL語句末尾應該使用分號結束。 1.1 SQL語句及相關操作示例 DDL&#xff1a;數據定義語言&#xff0c;負責數據庫定義、數據庫對象定義&#xff0c;由CREATE、ALTER與DROP三個語法所組成DML&#xff1a;數據操作語言&#xff…

關于小程序收集用戶手機號行為的規范

手機號在日常生活中被廣泛使用&#xff0c;是重要的用戶個人信息&#xff0c;小程序開發者應在用戶明確同意的前提下&#xff0c;依法合規地處理用戶的手機號信息。 而部分開發者在處理用戶手機號過程中&#xff0c;存在不規范收集行為&#xff0c;影響了用戶的正常使用體驗&a…

ElasticSearchConfig

1. 添加配置 <dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId></dependency>2. es 配置信息 import org.apache.http.HttpHost; import org.apache.http.auth.Au…