解析C++中std::ref的使用
1. 前言
關(guān)于c++中的std::ref,std::ref在c++11引入。
本文通過(guò)講解std::ref的常用方式,及剖析下std::ref內(nèi)部實(shí)現(xiàn),進(jìn)而再來(lái)講解下std::reference_wrapper,然后我們?cè)龠M(jìn)一步分析為什么使用std::ref。
2. std::ref 用法
簡(jiǎn)單舉例來(lái)說(shuō):
int n1 = 0; auto n2 = std::ref(n1); n2++; n1++; std::cout << n1 << std::endl; // 2 std::cout << n2 << std::endl; // 2
可以看到 是把n1的引用傳遞給了n2,分別進(jìn)行加法,可以看到n2是n1的引用,最終得到的值都是2
那么大家可能會(huì)想,我都已經(jīng)有了’int& a = b’的這種引用賦值的語(yǔ)法了,為什么c++11又出現(xiàn)了一個(gè)std::ref,我們繼續(xù)來(lái)看例子:
#include <iostream> #include <thread> void thread_func(int& n2) { // error, >> int n2 n2++; } int main() { int n1 = 0; std::thread t1(thread_func, n1); t1.join(); std::cout << n1 << std::endl; }
我們?nèi)绻麑懗蛇@樣是編譯不過(guò)的,除非是去掉引用符號(hào),那么我如果非要傳引用怎么辦呢?
// snap ... int main() { int n1 = 0; std::thread t1(thread_func, std::ref(n1)); t1.join(); std::cout << n1 << std::endl; // 1 }
這樣可以看到引用傳遞成功,并且能夠達(dá)到我們效果,我們?cè)賮?lái)看個(gè)例子:
#include <iostream> #include <functional> void func(int& n2) { n2++; } int main() { int n1 = 0; auto bind_fn = std::bind(&func, std::ref(n1)); bind_fn(); std::cout << n1 << std::endl; // 1 }
這里我們也發(fā)現(xiàn)std::bind這樣也是需要通過(guò)std::ref來(lái)實(shí)現(xiàn)bind引用。
那么我們其實(shí)可以看的出來(lái),std::bind或者std::thread里是做了什么導(dǎo)致我們?cè)瓉?lái)的通過(guò)&傳遞引用的方式失效,或者說(shuō)std::ref是做了什么才能使得我們使用std::bind和std::thread能夠傳遞引用。
那么我們展開(kāi)std::ref看看他的真面目,大致內(nèi)容如下:
template <class _Ty> reference_wrapper<_Ty> ref(_Ty& _Val) noexcept { return reference_wrapper<_Ty>(_Val); }
這里我們看到std::ref最終只是被包裝成reference_wrapper返回,所以關(guān)鍵點(diǎn)還是std::reference_wrapper
3. std::reference_wrapper
關(guān)于這個(gè)類,我們看下cppreference上的實(shí)現(xiàn)形式為:
namespace detail { template <class T> constexpr T& FUN(T& t) noexcept { return t; } template <class T> void FUN(T&&) = delete; } template <class T> class reference_wrapper { public: // types typedef T type; // construct/copy/destroy template <class U, class = decltype( detail::FUN<T>(std::declval<U>()), std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>() )> constexpr reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u)))) : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {} reference_wrapper(const reference_wrapper&) noexcept = default; // 賦值 reference_wrapper& operator=(const reference_wrapper& x) noexcept = default; // 訪問(wèn) constexpr operator T& () const noexcept { return *_ptr; } constexpr T& get() const noexcept { return *_ptr; } template< class... ArgTypes > constexpr std::invoke_result_t<T&, ArgTypes...> operator() ( ArgTypes&&... args ) const { return std::invoke(get(), std::forward<ArgTypes>(args)...); } private: T* _ptr; }; // deduction guides template<class T> reference_wrapper(T&) -> reference_wrapper<T>;
里邊有一些語(yǔ)法比較晦澀,我們一點(diǎn)一點(diǎn)的來(lái)看
最開(kāi)始是一個(gè)detail的namespace,里邊有兩個(gè)函數(shù),第一個(gè)是接收左值引用的,第二個(gè)是接收右值引用的,接收右值引用的被delete,不能調(diào)用。這里detail是為后邊做校驗(yàn)的,大家可能會(huì)像,不用右值引用不寫就可以了,為啥寫了這個(gè)函數(shù)還要標(biāo)記為delete。這是因?yàn)槿绻麤](méi)有第二個(gè)函數(shù)右值參數(shù)是可以傳遞給第一個(gè)函數(shù)的,如果寫了就會(huì)優(yōu)先匹配到到第二個(gè)函數(shù),發(fā)現(xiàn)這個(gè)函數(shù)是delete,不能編譯通過(guò),明白了這個(gè)我們繼續(xù)。
接著我們看到reference_wrapper,首先是一個(gè)模板,看到很長(zhǎng)的一個(gè)構(gòu)造函數(shù),我們拆開(kāi)來(lái)看,template <class U, class = xxx>這種寫法,后邊那個(gè)class=也是在編譯期做校驗(yàn)使用,SFINEA的一種實(shí)現(xiàn)形式吧,如果class=后邊那個(gè)編譯不過(guò),那么你就不可以使用這個(gè)構(gòu)造函數(shù)。
class=后邊這段很長(zhǎng)的代碼:
template <class U, class = decltype( detail::FUN<T>(std::declval<U>()), std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>() )>
首先是一個(gè)decltype關(guān)鍵字,得到的是一個(gè)類型。decltype內(nèi)部是使用逗號(hào)表達(dá)式連接兩部分,逗號(hào)左邊部分調(diào)用detail的FUN來(lái)校驗(yàn),std::declval是不用調(diào)用構(gòu)造函數(shù)便可以使用類的成員函數(shù),不過(guò)只能用于不求值語(yǔ)境。獲取U的對(duì)象看下是否是右值,上邊也說(shuō)到如果右值則編譯不過(guò)。
如果是左值的話看逗號(hào)右邊的部分,std::enable_if_t<>, 這里<>中的第一個(gè)參數(shù)是條件,如果條件滿足返回第二個(gè)參數(shù),第二個(gè)參數(shù)是類型, 這里沒(méi)有第二個(gè)參數(shù),默認(rèn)是void,即如果滿足條件可以編譯通過(guò),否則編譯不通過(guò)。條件是std::is_same_v取反,std::is_same_v<>是如果兩個(gè)模板參數(shù)相同類型則是true,否則false。
所以reference_wrapper和std::remove_cvref_t<U>不相同則可以通過(guò)編譯,std::remove_cvref_t這個(gè)模板又是去掉U這個(gè)類型的const,volatile和引用的屬性,單純兩個(gè)類型比較。
上邊總結(jié)就是在調(diào)用構(gòu)造函數(shù)時(shí),首先進(jìn)行校驗(yàn),傳入?yún)?shù)時(shí)右值和reference_wrapper類型就不能編譯通過(guò)。
然后是構(gòu)造函數(shù)的正文:
constexpr reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u)))) : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}
這里先看下“noexcept(noexcept(detail::FUN(std::forward<U>(u))))”這段代碼,不了解noexcept我這里大概講解下。
- 語(yǔ)法上來(lái)說(shuō)noexcept分為修飾符和操作符兩種分類吧。
- 修飾符寫法是noexcept(expression),expression是常量表達(dá)式,expression這個(gè)值返回true則編譯器認(rèn)為修飾的函數(shù)不拋出異常,這時(shí)如果該函數(shù)再拋出異常則調(diào)用std::abort終止程序,如果值返回false則認(rèn)為該函數(shù)可能會(huì)拋出異常。而我們常看到函數(shù)聲明后邊只寫一個(gè)noexcept,其實(shí)也是相當(dāng)于noexcept(true)。
- 操作符大都用于模板中,寫法就是我們這里縮寫的那樣noexcept(noexcept(T())),那么這里T()決定該函數(shù)是否拋出異常,如果T()會(huì)拋出異常那么第二個(gè)noexcept就會(huì)返回false,否則返回true。
那么這里構(gòu)造函數(shù)就是說(shuō)如果執(zhí)行“detail::FUN(std::forward<U>(u))”不會(huì)拋出異常,那么就不會(huì)拋出異常,這樣也是更好的告知編譯器一個(gè)條件吧。
繼續(xù)的就是_ptr存放的是傳進(jìn)來(lái)參數(shù)的地址,這里也是比較關(guān)鍵,相當(dāng)于是reference_wrapper的實(shí)現(xiàn)就是通過(guò)保存?zhèn)鬟M(jìn)來(lái)參數(shù)的地址來(lái)達(dá)到引用的包裝(ref wrapper)效果。
構(gòu)造函數(shù)終于講完了,拷貝構(gòu)造函數(shù)和賦值運(yùn)算符應(yīng)該不用講了
再然后就是看下如何訪問(wèn)了
constexpr operator T& () const noexcept { return *_ptr; } constexpr T& get() const noexcept { return *_ptr; }
這兩個(gè)也比較簡(jiǎn)單,提供了一個(gè)get函數(shù)和()的重載,實(shí)現(xiàn)就是獲取_ptr存放地址所指向的值。
template< class... ArgTypes > constexpr std::invoke_result_t<T&, ArgTypes...> operator() ( ArgTypes&&... args ) const { return std::invoke(get(), std::forward<ArgTypes>(args)...); }
還有一個(gè)實(shí)現(xiàn)是給存放的參數(shù)是函數(shù)類型使用的,也就是重載"()()",可以調(diào)用這個(gè)函數(shù)并傳參過(guò)去。
最后就是C++17引入的推導(dǎo)指引,顧名思義就是幫助模板類型推導(dǎo)使用的
推導(dǎo)指引 template<class T> reference_wrapper(T&) -> reference_wrapper<T>;
如果沒(méi)有這句話,我們構(gòu)造reference_wraper時(shí),需要這么寫reference_wraper<int>(n1),那么有了這句推導(dǎo)指引,我們可以寫成這樣reference_wraper(n1),方便很多,不用寫模板參數(shù)類型。
那么接下來(lái)我們調(diào)用試試看(因?yàn)閏ppreference中實(shí)現(xiàn)有些語(yǔ)法用到了C++17或者更高,使用編譯器要更高版本或者替換一些語(yǔ)法即可):
void func(int& n2) { n2++; } int main() { int n1 = 0; auto bind_fn = std::bind(&func, reference_wrapper(n1)); bind_fn(); std::cout << n1 << std::endl; // 1 }
完美!可以通過(guò), 所以reference_wrapper本質(zhì)是把對(duì)象的地址保存, 訪問(wèn)是取出地址的值。
這里我們借助的是cppreference中實(shí)現(xiàn)來(lái)講解的,大家也可以參考自己本地編譯器的實(shí)現(xiàn)。
4. 為什么使用
我們看下為什么std::bind或者std::thread為什么要使用reference_wrapper,我們以std::bind為例子吧,我們大致去跟蹤下std::bind,跟蹤的目的是看傳遞bound參數(shù)(即我們傳給bind函數(shù)的參數(shù))的生命周期,以vs2019的實(shí)現(xiàn)為例:
template <class _Fx, class... _Types> _NODISCARD _CONSTEXPR20 _Binder<_Unforced, _Fx, _Types...> bind(_Fx&& _Func, _Types&&... _Args) { return _Binder<_Unforced, _Fx, _Types...>(_STD forward<_Fx>(_Func), _STD forward<_Types>(_Args)...); }
看到是構(gòu)造了一個(gè)_Binder的對(duì)象返回,bound參數(shù)作為構(gòu)造函數(shù)的參數(shù)傳入,
using _Second = tuple<decay_t<_Types>...>; //std::decay_t會(huì)移除掉引用屬性 _Compressed_pair<_First, _Second> _Mypair; constexpr explicit _Binder(_Fx&& _Func, _Types&&... _Args) : _Mypair(_One_then_variadic_args_t{}, _STD forward<_Fx>(_Func), _STD forward<_Types>(_Args)...) {}
也可以看到構(gòu)造函數(shù)中,參數(shù)傳遞給_Mypair成員。到這里結(jié)束。
我們?cè)倏聪抡{(diào)用時(shí):
#define _CALL_BINDER \ _Call_binder(_Invoker_ret<_Ret>{}, _Seq{}, _Mypair._Get_first(), _Mypair._Myval2, \ _STD forward_as_tuple(_STD forward<_Unbound>(_Unbargs)...)) template <class... _Unbound> _CONSTEXPR20 auto operator()(_Unbound&&... _Unbargs) noexcept(noexcept(_CALL_BINDER)) -> decltype(_CALL_BINDER) { return _CALL_BINDER; }
看到調(diào)用時(shí)會(huì)用到_CALL_BINDER宏,這里調(diào)用_Call_binder函數(shù),并把_Mypair傳入,再接下來(lái)就會(huì)調(diào)用到我們的函數(shù)并傳入bound的參數(shù)了。
總結(jié)下就是std::bind首先將傳入的參數(shù)存放起來(lái),等到要調(diào)用bind的函數(shù)就將參數(shù)傳入,而這里沒(méi)有保存?zhèn)魅雲(yún)?shù)的引用,只能保存一份參數(shù)的拷貝,如果使用我們上邊說(shuō)的“int& a = b”語(yǔ)法,_Binder類中無(wú)法保存b的引用,自然調(diào)用時(shí)傳入的就不是b的引用,所以借助reference_wrapper將傳入?yún)?shù)的地址保存,使用是通過(guò)地址取出來(lái)值進(jìn)而調(diào)用函數(shù)。
5. 總結(jié)
我來(lái)給總結(jié)下,首先我們講解了std::ref的一些用法,然后我們講解std::ref是通過(guò)std::reference_wrapper實(shí)現(xiàn),然后我們借助了cppreference上的實(shí)現(xiàn)來(lái)給大家剖析了他本質(zhì)就是存放了對(duì)象的地址(類似指針的用法),還講解了noexcept等語(yǔ)法,最后我們講解了下std::bind為什么要使用到reference_wrapper.
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
C++線程安全容器stack和queue的使用詳細(xì)介紹
stack是一種容器適配器,專門用在具有后進(jìn)先出操作的上下文環(huán)境中,其刪除只能從容器的一端進(jìn)行 元素的插入與提取操作;隊(duì)列是一種容器適配器,專門用于在FIFO上下文(先進(jìn)先出)中操作,其中從容器一端插入元素,另一端提取元素2022-08-08深入淺析C/C++語(yǔ)言結(jié)構(gòu)體指針的使用注意事項(xiàng)
這篇文章主要介紹了C/C++語(yǔ)言結(jié)構(gòu)體指針的使用,大家都知道指針在32位系統(tǒng)占用4Byte,在64位系統(tǒng)占用8Byte,下面看下c語(yǔ)言代碼例子2021-12-12純C語(yǔ)言:遞歸二進(jìn)制轉(zhuǎn)十進(jìn)制源碼分享
這篇文章主要介紹了純C語(yǔ)言:遞歸二進(jìn)制轉(zhuǎn)十進(jìn)制源碼,有需要的朋友可以參考一下2014-01-01C++控制臺(tái)循環(huán)鏈表實(shí)現(xiàn)貪吃蛇
這篇文章主要為大家詳細(xì)介紹了C++控制臺(tái)循環(huán)鏈表實(shí)現(xiàn)貪吃蛇,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04深入解析C++中的函數(shù)模板和函數(shù)的默認(rèn)參數(shù)
這篇文章主要介紹了深入解析C++中的函數(shù)模板和函數(shù)的默認(rèn)參數(shù),是C++入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09JetBrains?CLion永久激活超詳細(xì)教程(最新激活方法)
JetBrains?Clion?是一款專為?C/C++?開(kāi)發(fā)所設(shè)計(jì)的跨平臺(tái)?IDE,本文適用?JetBrains?CLion?v2019.3/3.1/3.2/3.3?永久激活,附破解補(bǔ)丁和激活碼,可以永久激活?Windows、MAC、Linux?下的?CLion,下面給大家分享JetBrains?CLion永久激活超詳細(xì)教程,感興趣的朋友一起看看吧2023-01-01C++Primer筆記之關(guān)聯(lián)容器的使用詳解
本篇文章對(duì)C++Primer 關(guān)聯(lián)容器的使用進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下2013-05-05Qt實(shí)現(xiàn)字幕無(wú)間隙滾動(dòng)效果
這篇文章主要為大家詳細(xì)介紹了如何利用Qt實(shí)現(xiàn)字幕無(wú)間隙滾動(dòng)效果,文中的實(shí)現(xiàn)過(guò)程講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-11-11