[英]Why use std::forward in concepts?
我正在閱讀Constraints上的cppreference頁面並注意到這個例子:
// example constraint from the standard library (ranges TS)
template <class T, class U = T>
concept bool Swappable = requires(T t, U u) {
swap(std::forward<T>(t), std::forward<U>(u));
swap(std::forward<U>(u), std::forward<T>(t));
};
我很困惑他們為什么要使用std::forward
。 有些人試圖在模板參數中支持引用類型嗎? 我們不想用左值調用swap
,並且當T
和U
是標量(非引用)類型時, forward
表達式不會是rvalues嗎?
例如,考慮到他們的Swappable
實現,我希望這個程序失敗:
#include <utility>
// example constraint from the standard library (ranges TS)
template <class T, class U = T>
concept bool Swappable = requires(T t, U u) {
swap(std::forward<T>(t), std::forward<U>(u));
swap(std::forward<U>(u), std::forward<T>(t));
};
class MyType {};
void swap(MyType&, MyType&) {}
void f(Swappable& x) {}
int main()
{
MyType x;
f(x);
}
不幸的是,g ++ 7.1.0給了我一個內部編譯器錯誤 ,但這並未對此有所了解。
這里T
和U
都應該是MyType
,而std::forward<T>(t)
應該返回MyType&&
,它不能傳遞給我的swap
函數。
這個Swappable
實現錯了嗎? 我錯過了什么嗎?
我們不想用左值調用swap [...]
這是一個非常好的問題。 API設計的一個問題:概念庫的設計者應該賦予其概念參數什么含義或含義?
快速回顧一下可交換的需求。 也就是說,已經出現在今天的標准中的實際要求已經出現在概念之前:
- 當且僅當以下情況時,對象
t
可與對象u
交換 :
- [...]表達式
swap(t, u)
和swap(u, t)
是有效的[...][...]
右值或左值
t
是可交換的,當且僅當t是可交換與類型的任何右值或左值分別T
。
(摘錄了Swappable
要求[swappable.requirements],以減少大量不相關的細節。)
你抓到了嗎? 第一位提供符合您期望的要求。 變成一個實際的概念†也很簡單:
†:只要我們願意忽略大量超出我們范圍的細節
template<typename Lhs, typename Rhs = Lhs>
concept bool FirstKindOfSwappable = requires(Lhs lhs, Rhs rhs) {
swap(lhs, rhs);
swap(rhs, lhs);
};
現在,非常重要的是我們應該立即注意到這個概念支持開箱即用的參考變量:
int&& a_rref = 0;
int&& b_rref = 0;
// valid...
using std::swap;
swap(a_rref, b_rref);
// ...which is reflected here
static_assert( FirstKindOfSwappable<int&&> );
(現在從技術上來說,標准是根據參考文獻進行討論而不是參考文獻。由於參考文獻不僅涉及對象或功能,而且意味着透明地代表它們,我們實際上提供了一個非常理想的特征。實際上我們現在是在變量方面工作,而不僅僅是對象。)
這里有一個非常重要的連接: int&&
是我們變量的聲明類型,以及傳遞給概念的實際參數,而這反過來又會因為我們的lhs
和rhs
的聲明類型需要參數而再次結束。 隨着我們深入挖掘,請記住這一點。
那么提到左值和右值的那第二位呢? 好吧,這里我們不再處理變量,而是表達式 。 我們能為此寫一個概念嗎? 好吧,我們可以使用某種表達式到類型的編碼。 即decltype
使用的std::declval
以及另一個方向的std::declval
。 這導致我們:
template<typenaome Lhs, typename Rhs = Lhs>
concept bool SecondKindOfSwappable = requires(Lhs lhs, Rhs rhs) {
swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs));
swap(std::forward<Rhs>(rhs), std::forward<Lhs>(lhs));
// another way to express the first requirement
swap(std::declval<Lhs>(), std::declval<Rhs>());
};
這是你遇到的! 正如您所發現的那樣,必須以不同的方式使用該概念:
// not valid
//swap(0, 0);
// ^- rvalue expression of type int
// decltype( (0) ) => int&&
static_assert( !SecondKindOfSwappable<int&&> );
// same effect because the expression-decltype/std::declval encoding
// cannot properly tell apart prvalues and xvalues
static_assert( !SecondKindOfSwappable<int> );
int a = 0, b = 0;
swap(a, b);
// ^- lvalue expression of type int
// decltype( (a) ) => int&
static_assert( SecondKindOfSwappable<int&> );
如果你發現非顯而易見的話,那么看看這次的連接:我們有一個類型為int
的左值表達式,它被編碼為概念的int&
argument,它被std::declval<int&>()
恢復為我們約束中的表達式std::declval<int&>()
。 或者以更迂回的方式,通過std::forward<int&>(lhs)
。
cppreference條目上顯示的內容是Ranges TS指定的Swappable
概念的摘要。 如果我猜測,我會說Ranges TS決定讓Swappable
參數代表表達式,原因如下:
我們可以寫SecondKindOfSwappable
來講FirstKindOfSwappable
近以下給出:
template<typename Lhs, typename Rhs = Lhs> concept bool FirstKindOfSwappable = SecondKindOfSwappable<Lhs&, Rhs&>;
這個配方可以應用於許多但不是所有情況,使得有時可以根據在表達式隱藏類型中參數化的相同概念來表達在變量類型上參數化的概念。 但通常不可能采取相反的方式。
限制swap(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs))
預計是一個非常重要的場景; 在我的頭腦中,它出現在以下業務中:
template<typename Val, typename It> void client_code(Val val, It it) requires Swappable<Val&, decltype(*it)> // ^^^^^^^^^^^^^--. // | // hiding an expression into a type! ------` { ranges::swap(val, *it); }
一致性:在大多數情況下,TS的其他概念遵循相同的約定,並且針對表達式的類型進行參數化
但為什么大部分?
因為有第三種概念參數:代表...類型的類型。 一個很好的例子是DerivedFrom<Derived, Base>()
,它通常意義上沒有給出有效的表達式(或使用變量的方法)。
實際上,在例如Constructible<Arg, Inits...>()
,第一個參數Arg
可以用兩種方式解釋:
Arg
代表一種類型,即將可構造性作為一種類型的固有屬性 Arg
是正在構造的變量的聲明類型,即約束意味着Arg imaginary_var { std::declval<Inits>()... };
已驗證 最后我將以個人的方式結束:我認為讀者不應該(盡管)他們應該以相同的方式編寫自己的概念,因為表達式上的概念至少從概念編寫者的角度出現,是一個超集變量的概念。
還有其他因素在起作用,我關注的是從概念客戶的角度來看可用性以及我剛才提到的所有這些細節 。 但這並不是真的與這個問題有關,這個答案已經足夠長了,所以我會把這個故事留到另一個時間。
我對概念仍然很陌生,所以請隨意指出我在這個答案中需要解決的任何錯誤。 答案分為三個部分:第一部分直接關注std::forward
的使用,第二部分擴展了Swappable
,第三部分關注內部錯誤。
這似乎是一個錯字1 ,可能應該是requires(T&& t, U&& u)
。 在這種情況下,完美轉發用於確保對左值和右值引用正確評估該概念,從而保證只有左值引用將被標記為可交換。
完整的Ranges TS Swappable
概念完全定義為:
template <class T>
concept bool Swappable() {
return requires(T&& a, T&& b) {
ranges::swap(std::forward<T>(a), std::forward<T>(b));
};
}
template <class T, class U>
concept bool Swappable() {
return ranges::Swappable<T>() &&
ranges::Swappable<U>() &&
ranges::CommonReference<const T&, const U&>() &&
requires(T&& t, U&& u) {
ranges::swap(std::forward<T>(t), std::forward<U>(u));
ranges::swap(std::forward<U>(u), std::forward<T>(t));
};
}
約束和概念頁面上顯示的概念是這個的簡化版本,它似乎是作為庫概念Swappable
的最小實現。 由於完整定義指定了requires(T&&, U&&)
,因此這個簡化版本也應該是合理的。 因此使用std::forward
,期望t
和u
是轉發引用。
1: Cubbi在我測試代碼,做研究和吃晚餐時發表的評論證實這是一個錯字。
[以下擴展了Swappable
。 如果這與您無關,請隨意跳過它。]
需要注意的是,如果這部分僅適用Swappable
被命名空間之外定義std
; 如果在std
定義,因為它似乎在草稿中 ,則在重載解析期間將自動考慮兩個std::swap()
,這意味着不需要額外的工作來包含它們。 由於去Cubbi鏈接到的草案,並指出, Swappable
直接抽出來。
但請注意,簡化形式本身並不是Swappable
的完整實現,除非已經指定using std::swap
。 [swappable.requirements/3]
聲明重載解析必須考慮兩個std::swap()
模板和ADL找到的任何swap()
(即,解析必須像using std::swap
的using聲明一樣using std::swap
)已指定)。 由於概念不能包含using聲明,更完整的Swappable
可能如下所示:
template<typename T, typename U = T>
concept bool ADLSwappable = requires(T&& t, U&& u) {
swap(std::forward<T>(t), std::forward<U>(u));
swap(std::forward<U>(u), std::forward<T>(t));
};
template<typename T, typename U = T>
concept bool StdSwappable = requires(T&& t, U&& u) {
std::swap(std::forward<T>(t), std::forward<U>(u));
std::swap(std::forward<U>(u), std::forward<T>(t));
};
template<typename T, typename U = T>
concept bool Swappable = ADLSwappable<T, U> || StdSwappable<T, U>;
這個擴展的Swappable
將允許正確檢測滿足庫概念的參數, 如此 。
[以下是GCC的內部錯誤,與Swappable
本身沒有直接關系。 如果這與您無關,請隨意跳過它。]
但是,要使用它, f()
需要一些修改。 而不是:
void f(Swappable& x) {}
應改為使用以下其中一項:
template<typename T>
void f(T&& x) requires Swappable<T&&> {}
template<typename T>
void f(T& x) requires Swappable<T&> {}
這是由於GCC和概念解析規則之間的相互作用,並且可能會在未來版本的編譯器中進行整理。 使用約束表達式可以回避我認為對內部錯誤負責的交互,使其成為暫時可行的(如果更詳細的)權宜之計。
內部錯誤似乎是由GCC處理概念解析規則的方式引起的。 遇到此功能時:
void f(Swappable& x) {}
由於函數概念可以重載,因此在某些上下文中遇到概念名稱時會執行概念解析(例如,當用作約束類型說明符時,如此處的Swappable
)。 因此,GCC嘗試按照概念解析規則#1的規定, 在本頁的Concept resolution部分中解析Swappable
:
Swappable
,因此它將使用單個通配符作為其參數。 此通配符可以匹配任何可能的模板參數(無論是類型,非類型還是模板),因此是T
的完美匹配。 由於Swappable
的第二個參數與參數不對應,因此將使用其默認模板參數,如編號規則后指定的那樣; 我相信這是個問題。 由於T
是當前的(wildcard)
,一種簡單的方法是將U
臨時實例化為另一個通配符或第一個通配符的副本,並確定Swappable<(wildcard), (wildcard)>
與模式template<typename T, typename U>
匹配template<typename T, typename U>
(確實如此); 它然后可以推導出T
,並使用它來正確地確定它是否解析為Swappable
概念。
相反,GCC似乎已經達到了Catch-22:它不能實例化U
直到它推導出T
,但它不能推斷T
直到它確定這個Swappable
是否正確解析為Swappable
概念......它不能沒有U
。 因此,它需要弄清楚U
是什么才能確定我們是否擁有正確的Swappable
,但它需要知道我們是否擁有正確的Swappable
才能弄清楚U
是什么; 面對這個無法解決的難題,它有動脈瘤,龍骨,死亡。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.