C++:std::function的libc++實現

std::function是個有點神奇的模板,無論是普通函數、函數對象、lambda表達式還是std::bind的返回值(以上統稱為可調用對象(Callable)),無論可調用對象的實際類型是什么,無論是有狀態的還是無狀態的,只要它們有相同參數類型和返回值類型,就可以使用同一類型的std::function進行存儲和調用。這種特性被稱作類型擦除(Type erasure),它允許我們在不知道對象實際類型的情況下對對象進行存儲和操作。

在本文中,我將以std::functionlibc++實現(14.0版本)為例,分析std::function類型擦除的實現原理,以及實現一個精簡版的std::functionMyFunction

std::function如何實現類型擦除?

在不知道對象實際類型的情況下操作對象,有一種常規的手段可以實現這個功能,那就是多態,libc++版的std::function正是基于虛函數實現的。具體是如何實現的呢?我們可以從考察std::function在被調用時發生了什么作為這個問題的切入點。

對于以下代碼:

#include <functional>
#include <iostream>
int main() {std::function<void()> f = []() {  //std::cout << "Hello, world!" << std::endl;};f();return 0;
}

std::cout一行打斷點,運行,得到以下堆棧:

#0  main::$_0::operator() (this=0x7fffffffdb18) at /mnt/d/code/function_test/call.cpp:6
#1  0x0000555555557745 in std::__1::__invoke<main::$_0&> (__f=...) at /usr/lib/llvm-14/bin/../include/c++/v1/type_traits:3640
#2  0x00005555555576fd in std::__1::__invoke_void_return_wrapper<void, true>::__call<main::$_0&> (__args=...) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/invoke.h:61
#3  0x00005555555576cd in std::__1::__function::__alloc_func<main::$_0, std::__1::allocator<main::$_0>, void ()>::operator()() (this=0x7fffffffdb18) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:180
#4  0x0000555555556839 in std::__1::__function::__func<main::$_0, std::__1::allocator<main::$_0>, void ()>::operator()() (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:354
#5  0x0000555555558622 in std::__1::__function::__value_func<void ()>::operator()() const (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:507
#6  0x00005555555577d5 in std::__1::function<void ()>::operator()() const (this=0x7fffffffdb10) at /usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:1184
#7  0x00005555555562e5 in main () at /mnt/d/code/function_test/call.cpp:8

不考慮lambda本身,以及invoke相關的類,std::function實現相關的類有以下幾個:

  1. std::__1::function<void ()>
  2. std::__1::__function::__value_func<void ()>
  3. std::__1::__function::__func<main::$_0, std::__1::allocator<main::$_0>, void ()>
  4. std::__1::__function::__alloc_func<main::$_0, std::__1::allocator<main::$_0>, void ()>

lambda的類型被定義為了main::$_0,可以看出來,function__function::__value_func兩個模板類不依賴lambda實際類型,__function::__func__function::__alloc_func對lambda類型有依賴。

std::function

std::function看起,被聲明為擁有一個模板參數_Fp。我們使用的是它的特化版本,具有兩個模板參數,返回值類型_Rp和參數列表類型_ArgTypes(接下來幾個類也都是特化出來的,不再贅述)。它有一個__function::__value_func<_Rp(_ArgTypes...)>類型的成員__f_

template<class _Fp> class function;template<class _Rp, class ..._ArgTypes>
class function<_Rp(_ArgTypes...)>
{typedef __function::__value_func<_Rp(_ArgTypes...)> __func;__func __f_;...
};
...
template <class _Rp, class... _ArgTypes>
template <class _Fp, class>
function<_Rp(_ArgTypes...)>::function(_Fp __f) : __f_(_VSTD::move(__f)) {}

std::functionoperator()被調用時,它只是地把調用轉發給__f_

template<class _Rp, class ..._ArgTypes>
_Rp
function<_Rp(_ArgTypes...)>::operator()(_ArgTypes... __arg) const
{return __f_(_VSTD::forward<_ArgTypes>(__arg)...);
}

__function::__value_func

看看__function::__value_func具體是什么類型:

// __value_func creates a value-type from a __func.
template <class _Fp> class __value_func;template <class _Rp, class... _ArgTypes> class __value_func<_Rp(_ArgTypes...)>
{typename aligned_storage<3 * sizeof(void*)>::type __buf_;typedef __base<_Rp(_ArgTypes...)> __func;__func* __f_;...
};

它的模板參數和std::function一致,有兩個成員,一個成員是有3個指針大小的__buf_,另一個成員是__function::__base<_Rp(_ArgTypes...)>*類型的__f_

__function::__value_func的構造函數相對復雜一些,主要是為了做一個優化:當__f_指向的對象的大小小于等于__buf_的大小,也就是3個指針時,__f_會被構造在__buf_上,這樣可以減少堆上內存的分配:

template <class _Fp, class _Alloc>
__value_func(_Fp&& __f, const _Alloc& __a): __f_(nullptr)
{typedef allocator_traits<_Alloc> __alloc_traits;typedef __function::__func<_Fp, _Alloc, _Rp(_ArgTypes...)> _Fun;typedef typename __rebind_alloc_helper<__alloc_traits, _Fun>::type_FunAlloc;if (__function::__not_null(__f)){_FunAlloc __af(__a);if (sizeof(_Fun) <= sizeof(__buf_) &&is_nothrow_copy_constructible<_Fp>::value &&is_nothrow_copy_constructible<_FunAlloc>::value){__f_ =::new ((void*)&__buf_) _Fun(_VSTD::move(__f), _Alloc(__af));}else{typedef __allocator_destructor<_FunAlloc> _Dp;unique_ptr<__func, _Dp> __hold(__af.allocate(1), _Dp(__af, 1));::new ((void*)__hold.get()) _Fun(_VSTD::move(__f), _Alloc(__a));__f_ = __hold.release();}}
}

需要注意到的一個細節是:__f_在模板類定義中的類型是__function::__base,而此處new出來的對象類型是__function::__func,不難猜到,__function::__func繼承了__function::__base

__function::__value_funcoperator()被調用時,它也只是在做完合法性檢查后把調用轉發給了*__f_

_Rp operator()(_ArgTypes&&... __args) const
{if (__f_ == nullptr)__throw_bad_function_call();return (*__f_)(_VSTD::forward<_ArgTypes>(__args)...);
}

__function::__base

下面是__function::__base,它是一個抽象模板類,模板參數和std::function一致,不包含可調用對象的具體類型:

template<class _Fp> class __base;template<class _Rp, class ..._ArgTypes>
class __base<_Rp(_ArgTypes...)>
{__base(const __base&);__base& operator=(const __base&);
public:_LIBCPP_INLINE_VISIBILITY __base() {}_LIBCPP_INLINE_VISIBILITY virtual ~__base() {}virtual __base* __clone() const = 0;virtual void __clone(__base*) const = 0;virtual void destroy() _NOEXCEPT = 0;virtual void destroy_deallocate() _NOEXCEPT = 0;virtual _Rp operator()(_ArgTypes&& ...) = 0;
#ifndef _LIBCPP_NO_RTTIvirtual const void* target(const type_info&) const _NOEXCEPT = 0;virtual const std::type_info& target_type() const _NOEXCEPT = 0;
#endif // _LIBCPP_NO_RTTI
};

__function::__func

然后是__function::__func,它繼承了__function::__base,并且其模板參數含有可調用對象的類型_Fp,這正是實現類型擦除的關鍵:類型_Fp被隱藏了在了__function::__base這個抽象類后面。__function::__func含有一個類型為__function::__alloc_func的成員__f_

// __func implements __base for a given functor type.
template<class _FD, class _Alloc, class _FB> class __func;template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
class __func<_Fp, _Alloc, _Rp(_ArgTypes...)>: public  __base<_Rp(_ArgTypes...)>
{__alloc_func<_Fp, _Alloc, _Rp(_ArgTypes...)> __f_;
public:explicit __func(_Fp&& __f): __f_(_VSTD::move(__f)) {}...
};

__function::__funcoperator()依然只是轉發調用:

template<class _Fp, class _Alloc, class _Rp, class ..._ArgTypes>
_Rp
__func<_Fp, _Alloc, _Rp(_ArgTypes...)>::operator()(_ArgTypes&& ... __arg)
{return __f_(_VSTD::forward<_ArgTypes>(__arg)...);
}

__function::__alloc_func

然后是最后一個類__function::__alloc_func,它有一個pair類型的成員__f_std::function構造時傳入的可調用對象最終會存儲在__f_中:

// __alloc_func holds a functor and an allocator.
template <class _Fp, class _Ap, class _FB> class __alloc_func;template <class _Fp, class _Ap, class _Rp, class... _ArgTypes>
class __alloc_func<_Fp, _Ap, _Rp(_ArgTypes...)>
{__compressed_pair<_Fp, _Ap> __f_;public:...explicit __alloc_func(_Target&& __f): __f_(piecewise_construct, _VSTD::forward_as_tuple(_VSTD::move(__f)),_VSTD::forward_as_tuple()){}...
};

__function::__alloc_funcoperator()方法中,調用轉發給了__invoke_void_return_wrapper::__call,后面的流程就和std::function的實現無關了。

_Rp operator()(_ArgTypes&&... __arg)
{typedef __invoke_void_return_wrapper<_Rp> _Invoker;return _Invoker::__call(__f_.first(),_VSTD::forward<_ArgTypes>(__arg)...);
}

最終我們發現,“神奇”的類型擦除還是通過“樸素”的多態來實現的,之所以顯得神奇是因為多態被隱藏了起來,沒有暴露給用戶。

std::function對構造參數的校驗

仔細觀察一下std::function的構造函數:

template <class _Rp, class... _ArgTypes>
template <class _Fp, class>
function<_Rp(_ArgTypes...)>::function(_Fp __f) : __f_(_VSTD::move(__f)) {}

構造函數對參數__f似乎并沒有施加任何約束,如何真是那樣,那我們在使用一個不恰當的_Fp類型構造std::function時,很可能會得到可讀性極差的編譯錯誤信息,因為std::function類本身對_Fp沒有施加約束,那么實例化std::function時也就不太可能出現錯誤了,很有可能到了實例化__function::__alloc_func時編譯錯誤才會報告出來,這是一個內部類,一般用戶看到了關于它的實例化失敗的錯誤信息大概會感到摸不著頭腦。

但實際情況并不是這樣的,假設你這樣定義一個std::function對象:

std::function<void()> f(1);

你會得到一個比較清晰的編譯錯誤信息:

/mnt/d/code/function_test/myfunction.cpp:107:27: error: no matching constructor for initialization of 'std::function<void ()>'std::function<void()> f(1);
...
/usr/lib/llvm-14/bin/../include/c++/v1/__functional/function.h:998:5: note: candidate template ignored: requirement '__callable<int &, false>::value' was not satisfied [with _Fp = int]function(_Fp);
...

這是怎么做到的呢?答案藏在構造函數聲明的第二個模板參數class = _EnableIfLValueCallable<_Fp>

template<class _Fp, class = _EnableIfLValueCallable<_Fp>>
function(_Fp);

此處使用了SFINAE技術,我們看看_EnableIfLValueCallable具體是怎么實現的:

template <class _Fp, bool = _And<_IsNotSame<__uncvref_t<_Fp>, function>,__invokable<_Fp, _ArgTypes...>
>::value>
struct __callable;
template <class _Fp>struct __callable<_Fp, true>{static const bool value = is_void<_Rp>::value ||__is_core_convertible<typename __invoke_of<_Fp, _ArgTypes...>::type,_Rp>::value;};
template <class _Fp>struct __callable<_Fp, false>{static const bool value = false;};template <class _Fp>
using _EnableIfLValueCallable = typename enable_if<__callable<_Fp&>::value>::type;

_EnableIfLValueCallable的實現依賴于__callable__callable是一個模板類,擁有兩個模板參數,第一個模板參數_Fp是可調用對象的類型,第二個模板參數是bool類型的,當_IsNotSame<__uncvref_t<_Fp>, function>__invokable<_Fp, _ArgTypes...>這兩個條件同時滿足時,該模板參數為true,否則為false。

_IsNotSame<__uncvref_t<_Fp>, function>,顧名思義,是用來判斷兩個模板參數是否為同一類型的,這個條件似乎是為了避免歧義:當我們用另一個std::function構造std::function時,應該匹配到拷貝構造函數,而不是這個。

__invokable<_Fp, _ArgTypes...>則是用來判斷_Fp是否接受傳入_ArgTypes參數調用。

__callable第二個模板參數為false的特化中,將value直接定義為false。而模板參數為true的特化中,還添加了新的判斷條件,用來校驗可調用對象返回值的可轉換性。

第一個條件為is_void<_Rp>::value,用來判斷_Rpvoid類型。這意味著,即使可調用對象實際上有返回類型,但是std::function被定義為返回void,那么編譯也是可以通過的。

第二個條件是__is_core_convertible<typename __invoke_of<_Fp, _ArgTypes...>::type, _Rp>::value,用來判斷_Fp被調用后返回值可轉換為_Rp

綜上,_Fp要滿足以下條件,std::function的構造函數才能正常實例化:

_Fp不是std::function && _Fp可以以_ArgTypes為參數調用 && (_Rpvoid || _Fp返回值類型可轉換為_Rp)

這保證了當以不恰當的可調用對象構造std::function時,能夠盡可能提前觸發編譯錯誤,提升編譯錯誤信息的可讀性。

MyFunction的實現

下面我們下面模仿libc++,實現一個“青春版”的std::functionMyFunction,它忽略掉了大部分細節,只實現了構造和調用部分的代碼。

#include <functional>
#include <iostream>
#include <utility>template <typename Func>
class FunctionBase;template <typename Ret, typename... Args>
class FunctionBase<Ret(Args...)> {public:virtual Ret operator()(Args&&... args) = 0;
};template <typename Callable, typename Func>
class FunctionImpl;template <typename Callable, typename Ret, typename... Args>
class FunctionImpl<Callable, Ret(Args...)> : public FunctionBase<Ret(Args...)> {Callable c_;public:FunctionImpl(Callable&& c) : c_(std::move(c)) {}Ret operator()(Args&&... args) override {return std::invoke(c_, std::forward<Args>(args)...);}
};template <typename Func>
class MyFunction;template <typename Ret, typename... Args>
class MyFunction<Ret(Args...)> {FunctionBase<Ret(Args...)>* f_ = nullptr;public:template <typename Callable>MyFunction(Callable c) {f_ = new FunctionImpl<Callable, Ret(Args...)>(std::move(c));}Ret operator()(Args&&... args) {if (f_ == nullptr) {throw std::bad_function_call();}return (*f_)(std::forward<Args>(args)...);}
};void normalFunction() { std::cout << "I'm a normal function" << std::endl; }struct FunctionObject {void operator()() { std::cout << "I'm a function object" << std::endl; }
};int main() {MyFunction<void()> f0 = []() { std::cout << "I'm a lambda" << std::endl; };f0();MyFunction<void()> f1 = normalFunction;f1();MyFunction<void()> f2 = FunctionObject();f2();return 0;
}

結語

在沒有std::function可用的年代或者場合,我們一般會選擇使用函數指針來實現類似std::function的功能。在使用C實現的Linux內核代碼中,我們仍可以看到大量的函數指針的存在,主要是用來實現回調函數。

相較函數指針,std::function最明顯的優勢在于可以方便地存儲帶狀態的函數,而函數指針只能以比較丑陋的方式來實現這個特性。

其次是靈活性,std::function給客戶代碼施加的約束較小,我們可以使用任意形式的可調用對象:普通函數,lambda表達式,函數對象等,函數指針就沒有這種靈活性了。

不過由于虛函數的存在,std::function多了一點性能開銷,但這點開銷對大多數常規應用來說都是微不足道的。

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

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

相關文章

【C++】string基本用法(常用接口介紹)

文章目錄 一、string介紹二、string類對象的創建&#xff08;常見構造&#xff09;三、string類對象的容量操作1.size()和length()2.capacity()3.empty()4.clear()5.reserve()6.resize() 四、string類對象的遍歷與訪問1.operator[ ]2.正向迭代器begin()和end()3.反向迭代器rbeg…

QTableView與QSqlQueryModel的簡單使用

測試&#xff1a; 這里有一個sqlite數據庫 存儲了10萬多條數據&#xff0c;col1是1,col2是2. 使用QSqlQueryModel和QTableView來顯示這些數據&#xff0c;也非常非常流暢。 QString aFile QString::fromLocal8Bit("E:/桌面/3.db");if (aFile.isEmpty())return;//打…

關于考摩托車駕照

剛通過了摩托車駕照考試&#xff0c;說兩句。 1、在哪兒考試就要搞清楚當地的規定&#xff0c;不要以為全國要求都一樣。 2、首先是報駕校。雖然至少有些地方允許自學后&#xff08;不報駕校&#xff09;考試&#xff0c;但報駕校聽聽教練說的&#xff0c;還是能提高通過率&a…

計算機圖形學筆記----矩陣

矩陣和標量的運算 ,則 矩陣與矩陣相乘 的矩陣A&#xff0c;的矩陣B。兩矩陣&#xff0c;結果為的矩陣&#xff0c;第一個矩陣的列數必須和第二個矩陣的行數相同&#xff0c;否則不能相乘 &#xff0c;中的每個元素等于A的第i行所對應的矢量和B的第j列所對應的矢量進行矢量點…

Django靚號管理系統:實現用戶列表功能

在本篇博文中,我們將介紹如何在Django靚號管理系統中實現用戶列表功能。這個功能允許管理員查看系統中所有用戶的基本信息。我們將逐步講解如何設置URL路由、創建視圖函數以及設計模板。 1. 設置URL路由 首先,我們需要在??urls.py??文件中添加一個新的URL路徑,以便訪問…

云計算【第一階段(22)】Linux的進程和計劃任務管理

目錄 一、查看進程 1.1、程序和進程的關系 1.2、查看進程 1.2.1、靜態查看進程信息ps ?編輯 1.2.1.1、實驗 1.2.2、動態查看進程信息top 1.2.2.1、實驗 1.2.2.2、top 命令全屏操作界面快捷鍵 1.2.3、pgrep根據特定條件查詢進程pid信息 1.2.4、pstree命令以樹形結構列出…

CentOS系統日志入門

日志清單 系統的引導日志:/var/log/boot.log核心啟動日志:/var/log/dmesg系統報錯日志:/var/log/messages郵件系統日志:/var/log/maillogFTP系統日志:/var/log/xferlog安全信息和系統登錄與網絡連接的信息:/var/log/secureNews日志:/var/log/spoolerRPM軟件包:/var/log/rpmpkg…

Android 常用ADB命令

文章目錄 Android 常用ADB命令概述adb 的工作原理命令adb命令shell命令 使用adb服務器操作設備操作應用文件操作activity操作日志操作 Android 常用ADB命令 概述 Android 調試橋 (adb) 是一種功能多樣的命令行工具&#xff0c;可讓您與設備進行通信。adb 命令可用于執行各種設…

Avue框架學習

Avue框架學習 我們的項目使用的框架是 Avue 在我看來這個框架最大的特點是可以基于JSON配置頁面上的From,Table以及各種各樣的輸入框等,不需要懂前端就可以很快上手,前提是需要多查一下文檔 開發環境搭建 由于我本地的環境全是用docker來搭建的,所以我依然選擇用docker搭建我…

萬字淺析視頻搜索系統中的多模態能力建設

萬字淺析視頻搜索系統中的多模態能力建設 FesianXu 20240331 at Tencent WeChat search team 前言 視頻搜索是天然的富媒體檢索場景&#xff0c;視覺信息占據了視頻的一大部分信息量&#xff0c;在視頻搜索系統中引入多模態能力&#xff0c;對于提高整個系統的能力天花板至關重…

機器人控制系列教程之任務空間運動控制器搭建(1)

任務空間運動控制簡介 任務空間運動控制—位置被指定給控制器作為末端執行器的姿態。然后&#xff0c;控制器驅動機器人的關節配置到使末端執行器移動到指定姿態的值。這有時被稱為操作空間控制。 任務空間運動模型表示機器人在閉環任務空間位置控制下的運動&#xff0c;可使用…

python基礎:高級數據類型:集合

1、集合的定義 集合是一個無序且無重復元素的列表。其定義與數學定義一致。其無序和不重復和字典特征類似&#xff0c;但是無“值”。 2、集合的創建 集合一般由列表創建&#xff0c;在初始化列表時保證其元素唯一性&#xff0c;即為集合。 創建方法&#xff1a;x set(list…

汽車電子工程師入門系列——AUTOSAR通信服務框架(下)

我是穿拖鞋的漢子,魔都中堅持長期主義的汽車電子工程師。 老規矩,分享一段喜歡的文字,避免自己成為高知識低文化的工程師: 屏蔽力是信息過載時代一個人的特殊競爭力,任何消耗你的人和事,多看一眼都是你的不對。非必要不費力證明自己,無利益不試圖說服別人,是精神上的節…

GitHub每周最火火火項目(6.24-6.30)

項目名稱&#xff1a;dortania / OpenCore - Legacy - Patcher 項目介紹&#xff1a;該項目旨在讓用戶體驗如同以前一樣的macOS。它可能提供了一種方式來解決在某些情況下&#xff0c;用戶無法正常使用或升級macOS的問題。通過使用OpenCore - Legacy - Patcher&#xff0c;用戶…

python格式文件

python小白考后復習 CSV格式文件ini格式文件我們可以讀取所有節點還可以輸出一個節點下所有鍵值對組成的元組獲取節點下的鍵對應的值判斷節點是否存在添加節點還可以添加鍵值還可以刪除節點 XML格式文件讀取若是文件格式存在的xml若是以字符串形式存在的xml獲取子標簽還有獲取子…

【分布式計算框架 MapReduce】高級編程—搜索日志數據分析

目錄 一、對于 sogou_500w_utf 數據&#xff0c;使用 MapReduce 編程模型完成對以下數據的分析任務 1. 統計 2011-12-30 日搜索記錄&#xff0c;每個時間段的搜索次數 &#xff08;1&#xff09;運行截圖 &#xff08;2&#xff09; 源代碼 2. 統計 2011-12-30 日 3 點至 …

2024最新初級會計職稱題庫來啦!!!

16.根據增值稅法律制度的規定&#xff0c;下列各項中&#xff0c;屬于"提供加工、修理修配勞務"的是&#xff08;&#xff09;。 A.修理小汽車 B.修繕辦公樓 C.爆破 D.礦山穿孔 答案&#xff1a;A 解析&#xff1a;選項AB&#xff1a;修理有形動產&#xff08;…

【PL理論深化】(13) 變量與環境:文法結構 | 真假表達式:isZero E | let 表達式疊放 | 定義的規則 | 條件語句的使用

&#x1f4ac; 寫在前面&#xff1a;從現在開始&#xff0c;讓我們正式設計和實現編程語言。首先&#xff0c;讓我們擴展在之前定義的整數表達式語言&#xff0c;以便可以使用變量和條件表達式。 目錄 0x00 文法結構 0x01 真假表達式&#xff1a;isZero E 0x02 let 表達式疊…

Elasticsearch 第四期:搜索和過濾

序 2024年4月&#xff0c;小組計算建設標簽平臺&#xff0c;使用ES等工具建了一個demo&#xff0c;由于領導變動關系&#xff0c;項目基本夭折。其實這兩年也陸陸續續接觸和使用過ES&#xff0c;兩年前也看過ES的官網&#xff0c;當時剛畢業半年多&#xff0c;由于歷史局限性導…

Java有沒有goto語句

一、技術細節 Java作為一種廣泛使用的編程語言&#xff0c;其設計哲學強調清晰、簡潔和易于維護。在技術細節層面&#xff0c;Java確實支持goto語句作為關鍵字&#xff0c;但在實際編程中&#xff0c;goto語句并不被推薦使用。Java中的goto是保留字&#xff0c;主要用于兩個場…