[英]Compilers work differently for overload resolution with user-defined conversion to rvalue reference
當 gcc 和 clang select 不同的重載構造函數從用戶定義的轉換運算符隱式轉換時,我遇到了一個奇怪的行為。
有關代碼在這里:
#include <cstdio>
#include <utility>
#include <type_traits>
template <typename T>
class foo {
T& t_;
public:
foo(T& t) : t_(t) {}
operator T() const & { return t_; }
operator T&&() && { return std::move(t_); }
};
class bar {
int val_;
public:
bar(int v) : val_(v) {}
bar(const bar& b) : val_(b.val_) { printf("copy constructed\n"); }
bar& operator=(const bar& b) { printf("copy assigned\n"); val_ = b.val_; return *this; }
bar(bar&& mv) : val_(mv.val_) { printf("move constructed\n"); mv.val_ = -1; }
bar& operator=(bar&& mv) { printf("move assigned\n"); val_ = mv.val_; mv.val_ = -1; return *this; }
};
int main() {
bar v(1);
foo<bar> f(v);
bar v2(std::move(f));
}
class foo
是類型T
的包裝器,如果它是右值引用(當由std::move
轉換時),則應隱式轉換為T
或T&&
。
但是,一些編譯器更喜歡T
而不是T&&
,即使它是右值引用。 結果看起來像:
⟩ clang++-15 -std=c++17 test.cpp && ./a.out
copy constructed
⟩ clang++-15 -std=c++14 test.cpp && ./a.out
move constructed
⟩ g++ -std=c++14 test.cpp && ./a.out
move constructed
⟩ g++ -std=c++17 test.cpp && ./a.out
move constructed
僅對於 clang 和-std=c++17
或更高版本,復制構造函數優於移動構造函數。
編譯器版本:
⟩ g++ --version
g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
⟩ clang++-15 --version
Ubuntu clang version 15.0.4-++20221102053308+5c68a1cb1231-1~exp1~20221102053355.92
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
為什么不同編譯器和 C++ 版本的優先級不同? 還是我違反了一些規則?
在
bar v2(std::move(f));
頂級重載決議在bar
的復制構造函數和移動構造函數之間。 bar
確實有第三個構造函數,它采用int
,但顯然這不是選定的構造函數,所以我將忽略它。
無論選擇foo<bar>
的哪個轉換 function,該轉換 function 的結果都將是一個右值,因此bar
的移動構造函數將是首選。 話雖如此,我也將忽略復制構造函數而不深入研究標准。 (似乎移動構造函數的使用並不是這里爭議的一部分。盡管 OP 的程序打印“copy constructed”,但此復制構造是由operator T
完成的;復制構造函數不是在v2
本身上調用的構造函數。 ) 問題是將使用什么隱式轉換序列將類型foo<bar>
的右值轉換為參數類型bar&&
。 由於問題被標記為 [c++17],因此所有引用都將指向 C++17 標准。
根據 [over.best.ics]/1,隱式轉換序列“受 object 的初始化規則或單個表達式引用的規則約束”。 因此,我們必須參考 [dcl.init.ref],它管理引用的初始化。 正在初始化的引用具有類型bar&&
,並且初始化是來自類型foo<bar>
的右值的復制初始化。 達到的情況在該部分的 p5.2.1 中:
如果初始化表達式
- [不適用的情況被刪除],或
- 具有 class 類型(即
T2
是 class 類型),其中T1
與T2
不相關,並且可以轉換為右值或 function 類型“ cv3T3
”的左值,其中“ cv1T1
”是引用兼容的帶有“ cv3T3
”(見 16.3.1.6),然后 [...] 轉換的結果 [...] 稱為轉換后的初始值設定項。 如果轉換后的初始值設定項是純右值,則將其類型
T4
調整為類型“ cv1T4
” (7.5) 並應用臨時物化轉換 (7.4)。 在任何情況下,引用都綁定到生成的 glvalue(或適當的基 class 子對象)。
我們知道初始化器可以轉換為bar
類型的 xvalue 或 prvalue,並且bar
與自身引用兼容,因此將執行轉換並將引用綁定到該轉換的結果。 唯一的問題是轉換是如何完成的:通過operator bar
還是operator bar&&
?
要回答這個問題,我們必須查看參考部分 16.3.1.6,也稱為 [over.match.ref]:
在 11.6.3 中指定的條件下,引用可以直接綁定到 glvalue 或 class prvalue,這是將轉換 function 應用於初始化表達式的結果。 重載解析用於將select轉換為function來調用。 假設“reference to cv1
T
”為被初始化引用的類型,“ cvS
”為初始化表達式的類型,S
為class類型,候選函數選擇如下:
- 考慮了
S
及其基類的轉換函數。 那些未隱藏在S
中的非顯式轉換函數和產生類型“對cv2T2
的左值引用”(當初始化對函數的左值引用或右值引用時)或“ cv2T2
”或“對cv2T2
的右值引用”(當初始化函數的右值引用或左值引用),其中“ cv1T
”與“ cv2T2
”引用兼容(11.6.3),是候選函數。 對於直接初始化,[...]參數列表有一個參數,即初始化表達式。 [注意:此參數將與轉換函數的隱式 object 參數進行比較。 ——尾注]
這里, T
是 cv 非限定引用類型,即bar
。 有一種轉換 function 會產生bar
(即T2
,其中T2
是bar
),還有一種會產生bar&&
(即“對T2
的右值引用”,其中T2
是bar
)。 由於bar
在這兩種情況下都與T2
(即bar
本身)引用兼容,因此兩個轉換函數都是候選函數。 必須執行過載解析以確定調用哪一個。 這里,參數仍然是std::move(f)
,即foo<bar>
類型的右值,但參數是隱含的 object 參數:
operator T
,隱含的 object 參數具有類型foo<bar> const &
因為運算符是用const &
聲明的。operator T&&
,隱含的 object 參數具有類型foo<bar>&&
因為運算符是用&&
聲明的。 為了執行重載決議,我們考慮各自的隱式轉換序列,然后嘗試確定一個是否比另一個更好。 兩者都是身份轉換,因為foo<bar>
的右值可以直接綁定到foo<bar> const &
或foo<bar>&&
(參見 [over.ics.ref]/1)。 但正如我們所知,將右值引用綁定到右值比將左值引用綁定到右值要好。 這是規則 [over.ics.rank]/3.2.3:
標准轉換序列
S1
是比標准轉換序列S2
更好的轉換序列,如果
- [...] 或者,如果不是那樣,
- [...] 或者,如果不是那樣,
S1
和S2
是引用綁定 (11.6.3) 並且都沒有引用在沒有ref-qualifier的情況下聲明的非靜態成員 function 的隱式 object 參數,S1
將右值引用綁定到右值,S2
綁定左值引用,或者,如果不是那樣,[...]
(這里的附帶條件不適用;兩個函數都有ref-qualifiers )。
由於隱式 object 參數的隱式轉換序列在operator T&&
的情況下比operator T
符 T 更好,因此前者是最可行的 function。
GCC 是正確的。 應該調用operator T&&
,然后調用移動構造函數。 Clang 在 C++17 模式下調用operator T
(它在內部執行復制構造)。 沒有調用移動構造函數,因為 Clang 已經省略了它。
這是我們的線索,Clang 的行為可能與核心問題 2327有關。 我認為 Clang 維護者可能已經樂觀地實施了一些建議的解決方案來解決這個問題(盡管 Richard Smith 似乎沒有提供詳細的措辭)。 如果問題以與您所看到的行為一致的方式得到解決,並且委員會批准它作為針對 C++17 的缺陷報告,則 Clang 的行為將被認為是正確的。 另一方面,如果它以與 GCC 行為一致的方式解決,則 Clang 可能必須更改。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.