1. 前言
關于c++中的std::ref,std::ref在c++11引入,本文通過講解std::ref的常用方式,及剖析下std::ref內部實作,進而再來講解下std::reference_wrapper,然后我們再進一步分析為什么使用std::ref,
2. std::ref 用法
簡單舉例來說:
int n1 = 0;
auto n2 = std::ref(n1);
n2++;
n1++;
std::cout << n1 << std::endl; // 2
std::cout << n2 << std::endl; // 2
可以看到 是把n1的參考傳遞給了n2,分別進行加法,可以看到n2是n1的參考,最終得到的值都是2
那么大家可能會想,我都已經有了’int& a = b’的這種參考賦值的語法了,為什么c++11又出現了一個std::ref,我們繼續來看例子:
#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;
}
我們如果寫成這樣是編譯不過的,除非是去掉參考符號,那么我如果非要傳參考怎么辦呢?
// snap ...
int main() {
int n1 = 0;
std::thread t1(thread_func, std::ref(n1));
t1.join();
std::cout << n1 << std::endl; // 1
}
這樣可以看到參考傳遞成功,并且能夠達到我們效果,我們再來看個例子:
#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
}
這里我們也發現std::bind這樣也是需要通過std::ref來實作bind參考,
那么我們其實可以看的出來,std::bind或者std::thread里是做了什么導致我們原來的通過&傳遞參考的方式失效,或者說std::ref是做了什么才能使得我們使用std::bind和std::thread能夠傳遞參考,
那么我們展開std::ref看看他的真面目,大致內容如下:
template <class _Ty>
reference_wrapper<_Ty> ref(_Ty& _Val) noexcept {
return reference_wrapper<_Ty>(_Val);
}
這里我們看到std::ref最終只是被包裝成reference_wrapper回傳,所以關鍵點還是std::reference_wrapper
3. std::reference_wrapper
關于這個類,我們看下cppreference上的實作形式為:
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;
// 訪問
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>;
里邊有一些語法比較晦澀,我們一點一點的來看
最開始是一個detail的namespace,里邊有兩個函式,第一個是接收左值參考的,第二個是接收右值參考的,接收右值參考的被delete,不能呼叫,這里detail是為后邊做校驗的,大家可能會像,不用右值參考不寫就可以了,為啥寫了這個函式還要標記為delete,這是因為如果沒有第二個函式右值引數是可以傳遞給第一個函式的,如果寫了就會優先匹配到到第二個函式,發現這個函式是delete,不能編譯通過,明白了這個我們繼續,
接著我們看到reference_wrapper,首先是一個模板,看到很長的一個建構式,我們拆開來看,template <class U, class = xxx>這種寫法,后邊那個class=也是在編譯期做校驗使用,SFINEA的一種實作形式吧,如果class=后邊那個編譯不過,那么你就不可以使用這個建構式,
class=后邊這段很長的代碼:
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>>>()
)>
首先是一個decltype關鍵字,得到的是一個型別,decltype內部是使用逗號運算式連接兩部分,逗號左邊部分呼叫detail的FUN來校驗,std::declval是不用呼叫建構式便可以使用類的成員函式,不過只能用于不求值語境,獲取U的物件看下是否是右值,上邊也說到如果右值則編譯不過,如果是左值的話看逗號右邊的部分,std::enable_if_t<>, 這里<>中的第一個引數是條件,如果條件滿足回傳第二個引數,第二個引數是型別, 這里沒有第二個引數,默認是void,即如果滿足條件可以編譯通過,否則編譯不通過,條件是std::is_same_v取反,std::is_same_v<>是如果兩個模板引數相同型別則是true,否則false,所以reference_wrapper和std::remove_cvref_t<U>不相同則可以通過編譯,std::remove_cvref_t這個模板又是去掉U這個型別的const,volatile和參考的屬性,單純兩個型別比較,
上邊總結就是在呼叫建構式時,首先進行校驗,傳入引數時右值和reference_wrapper型別就不能編譯通過,
然后是建構式的正文:
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我這里大概講解下,
語法上來說noexcept分為修飾符和運算子兩種分類吧,
修飾符寫法是noexcept(expression),expression是常量運算式,expression這個值回傳true則編譯器認為修飾的函式不拋出例外,這時如果該函式再拋出例外則呼叫std::abort終止程式,如果值回傳false則認為該函式可能會拋出例外,而我們常看到函式宣告后邊只寫一個noexcept,其實也是相當于noexcept(true),
運算子大都用于模板中,寫法就是我們這里縮寫的那樣noexcept(noexcept(T())),那么這里T()決定該函式是否拋出例外,如果T()會拋出例外那么第二個noexcept就會回傳false,否則回傳true,
那么這里建構式就是說如果執行“detail::FUN(std::forward<U>(u))”不會拋出例外,那么就不會拋出例外,這樣也是更好的告知編譯器一個條件吧,
繼續的就是_ptr存放的是傳進來引數的地址,這里也是比較關鍵,相當于是reference_wrapper的實作就是通過保存傳進來引數的地址來達到參考的包裝(ref wrapper)效果,
建構式終于講完了,拷貝建構式和賦值運算子應該不用講了
再然后就是看下如何訪問了
constexpr operator T& () const noexcept { return *_ptr; }
constexpr T& get() const noexcept { return *_ptr; }
這兩個也比較簡單,提供了一個get函式和()的多載,實作就是獲取_ptr存放地址所指向的值,
template< class... ArgTypes >
constexpr std::invoke_result_t<T&, ArgTypes...>
operator() ( ArgTypes&&... args ) const {
return std::invoke(get(), std::forward<ArgTypes>(args)...);
}
還有一個實作是給存放的引數是函式型別使用的,也就是多載"()()",可以呼叫這個函式并傳參過去,
最后就是C++17引入的推導指引,顧名思義就是幫助模板型別推導使用的
推導指引
template<class T>
reference_wrapper(T&) -> reference_wrapper<T>;
如果沒有這句話,我們構造reference_wraper時,需要這么寫reference_wraper(n1),那么有了這句推導指引,我們可以寫成這樣reference_wraper(n1),方便很多,不用寫模板引數型別,
那么接下來我們呼叫試試看(因為cppreference中實作有些語法用到了C++17或者更高,使用編譯器要更高版本或者替換一些語法即可):
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
}
完美!可以通過, 所以reference_wrapper本質是把物件的地址保存, 訪問是取出地址的值,
這里我們借助的是cppreference中實作來講解的,大家也可以參考自己本地編譯器的實作,
4. 為什么使用
我們看下為什么std::bind或者std::thread為什么要使用reference_wrapper,我們以std::bind為例子吧,我們大致去跟蹤下std::bind,跟蹤的目的是看傳遞bound引數(即我們傳給bind函式的引數)的生命周期,以vs2019的實作為例:
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)...);
}
看到是構造了一個_Binder的物件回傳,bound引數作為建構式的引數傳入,
using _Second = tuple<decay_t<_Types>...>; //std::decay_t會移除掉參考屬性
_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)...) {}
也可以看到建構式中,引數傳遞給_Mypair成員,到這里結束,
我們再看下呼叫時:
#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;
}
看到呼叫時會用到_CALL_BINDER宏,這里呼叫_Call_binder函式,并把_Mypair傳入,再接下來就會呼叫到我們的函式并傳入bound的引數了,
總結下就是std::bind首先將傳入的引數存放起來,等到要呼叫bind的函式就將引數傳入,而這里沒有保存傳入引數的參考,只能保存一份引數的拷貝,如果使用我們上邊說的“int& a = b”語法,_Binder類中無法保存b的參考,自然呼叫時傳入的就不是b的參考,所以借助reference_wrapper將傳入引數的地址保存,使用是通過地址取出來值進而呼叫函式,
5. 總結
我來給總結下,首先我們講解了std::ref的一些用法,然后我們講解std::ref是通過std::reference_wrapper實作,然后我們借助了cppreference上的實作來給大家剖析了他本質就是存放了物件的地址(類似指標的用法😁),還講解了noexcept等語法,最后我們講解了下std::bind為什么要使用到reference_wrapper.
6. 參考
- https://en.cppreference.com
- 《深入理解C++11》
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/301122.html
標籤:其他
上一篇:Git與Unity的作業環境搭建
下一篇:【Java】 三國大亂斗部分代碼
