簡體   English   中英

編譯器通過用戶定義的右值引用轉換以不同方式處理重載決議

[英]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轉換時),則應隱式轉換為TT&&

但是,一些編譯器更喜歡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 類型),其中T1T2不相關,並且可以轉換為右值或 function 類型“ cv3 T3 ”的左值,其中“ cv1 T1 ”是引用兼容的帶有“ cv3 T3 ”(見 16.3.1.6),

然后 [...] 轉換的結果 [...] 稱為轉換后的初始值設定項。 如果轉換后的初始值設定項是純右值,則將其類型T4調整為類型“ cv1 T4 ” (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 ”為被初始化引用的類型,“ cv S ”為初始化表達式的類型, S為class類型,候選函數選擇如下:

  • 考慮了S及其基類的轉換函數。 那些未隱藏在S中的非顯式轉換函數和產生類型“對cv2 T2的左值引用”(當初始化對函數的左值引用或右值引用時)或“ cv2 T2 ”或“對cv2 T2的右值引用”(當初始化函數的右值引用或左值引用),其中“ cv1 T ”與“ cv2 T2 ”引用兼容(11.6.3),是候選函數。 對於直接初始化,[...]

參數列表有一個參數,即初始化表達式。 [注意:此參數將與轉換函數的隱式 object 參數進行比較。 ——尾注]

這里, T是 cv 非限定引用類型,即bar 有一種轉換 function 會產生barT2 ,其中T2bar ),還有一種會產生bar&&“對T2的右值引用”,其中T2bar )。 由於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更好的轉換序列,如果

  • [...] 或者,如果不是那樣,
  • [...] 或者,如果不是那樣,
  • S1S2是引用綁定 (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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM