std::function
是個有點神奇的模板,無論是普通函數、函數對象、lambda表達式還是std::bind
的返回值(以上統稱為可調用對象(Callable)),無論可調用對象的實際類型是什么,無論是有狀態的還是無狀態的,只要它們有相同參數類型和返回值類型,就可以使用同一類型的std::function
進行存儲和調用。這種特性被稱作類型擦除(Type erasure),它允許我們在不知道對象實際類型的情況下對對象進行存儲和操作。
在本文中,我將以std::function
的libc++
實現(14.0版本)為例,分析std::function
類型擦除的實現原理,以及實現一個精簡版的std::function
:MyFunction
。
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
實現相關的類有以下幾個:
std::__1::function<void ()>
std::__1::__function::__value_func<void ()>
std::__1::__function::__func<main::$_0, std::__1::allocator<main::$_0>, void ()>
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::function
的operator()
被調用時,它只是地把調用轉發給__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_func
的operator()
被調用時,它也只是在做完合法性檢查后把調用轉發給了*__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::__func
的operator()
依然只是轉發調用:
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_func
的operator()
方法中,調用轉發給了__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
,用來判斷_Rp
為void
類型。這意味著,即使可調用對象實際上有返回類型,但是std::function
被定義為返回void
,那么編譯也是可以通過的。
第二個條件是__is_core_convertible<typename __invoke_of<_Fp, _ArgTypes...>::type, _Rp>::value
,用來判斷_Fp
被調用后返回值可轉換為_Rp
。
綜上,_Fp
要滿足以下條件,std::function
的構造函數才能正常實例化:
_Fp
不是std::function
&& _Fp
可以以_ArgTypes
為參數調用 && (_Rp
為void
|| _Fp
返回值類型可轉換為_Rp
)
這保證了當以不恰當的可調用對象構造std::function
時,能夠盡可能提前觸發編譯錯誤,提升編譯錯誤信息的可讀性。
MyFunction的實現
下面我們下面模仿libc++
,實現一個“青春版”的std::function
:MyFunction
,它忽略掉了大部分細節,只實現了構造和調用部分的代碼。
#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
多了一點性能開銷,但這點開銷對大多數常規應用來說都是微不足道的。