2017-06-01 62 views
14

考慮下面的拷貝初始化:形式 '= {}'

#include <stdio.h> 

class X; 

class Y 
{ 
public: 
    Y() { printf(" 1\n"); }    // 1 
    // operator X(); // 2 
}; 

class X 
{ 
public: 
    X(int) {} 
    X(const Y& rhs) { printf(" 3\n"); } // 3 
    X(Y&& rhs) { printf(" 4\n"); }  // 4 
}; 

// Y::operator X() { printf(" operator X() - 2\n"); return X{2}; } 

int main() 
{ 
    Y y{};  // Calls (1) 

    printf("j\n"); 
    X j{y}; // Calls (3) 
    printf("k\n"); 
    X k = {y}; // Calls (3) 
    printf("m\n"); 
    X m = y; // Calls (3) 
    printf("n\n"); 
    X n(y); // Calls (3) 

    return 0; 
} 

到目前爲止,一切都很好。現在,如果我能轉換操作符Y::operator X(),我得到這個; -

X m = y; // Calls (2) 

我的理解是,這是因爲(2)是「少常量」比(3)和 因此優選。對X構造函數的調用被取消

我的問題是,爲什麼定義X k = {y}以相同的方式改變其行爲?我知道= {}在技術上是'列表副本初始化',但是在沒有構造函數的情況下使用initializer_list類型,是不是會恢復'複製初始化'行爲?即 - 與X m = y

我的理解在哪裏?

+0

對不起 - 我搞砸了最小化代碼 - 更正(希望) – Rich

+0

使用Exaxt編譯器?我知道在某些編譯器中與此相鄰的代碼不一致。 – Yakk

+0

改進了這個例子。我已經在11,14和17模式下用cppreference.com(clang 3.8)上的編譯器工具試過了。結果相同 – Rich

回答

7

我的理解在哪裏?

tltldr;沒人理解初始化。

tldr;列表初始化更喜歡std::initializer_list<T>構造函數,但它不會回退到非列表初始化。它只是回到考慮構造函數。非列表初始化將考慮轉換函數,但後退不會。


所有初始化規則來自[dcl.init]。所以讓我們從第一原則出發吧。

[dcl.init]/17.1

  • 如果初始化爲(非括號內)支撐-INIT列表或爲= 支撐-INIT列表,對象或參考是列表初始化。

第一個第一個項目符號點包含任何列表初始化。這跳躍X x{y}X x = {y}[dcl.init.list]。我們會回到這個。另一種情況更容易。我們來看看X x = y。我們稱之爲直降成:

[dcl.init]/17.6.3

  • 否則(即,對剩餘的副本初始化的情況下),可以從源類型轉換爲目標類型用戶自定義轉換序列或(當使用轉換函數時)到其派生類被枚舉如[over.match.copy]中所述,並且通過重載分辨率來選擇最好的類。

在[over.match。副本]是:

  • T的轉換構造函數[在我們的情況下,X]是候選功能。
  • 當初始化表達式的類型是類類型「cvS」時,將考慮S及其基類的非顯式轉換函數。

在這兩種情況下,參數列表都有一個參數,它是初始化表達式。

這給我們的候選人:

X(Y const &);  // from the 1st bullet 
Y::operator X(); // from the 2nd bullet 

第二屆相當於已患有X(Y&),因爲轉換功能是不是CV-合格。這會導致比轉換構造函數更少的cv限定引用,所以它是首選。請注意,在C++ 17中沒有調用X(X&&)


現在讓我們回到列表初始化情況。第一個相關的圓點是[dcl.init.list]/3.6

否則,如果T是類類型,構造函數被考慮。列舉適用的構造函數,並通過重載解析([over.match],[over.match.list])選擇最好的構造函數。如果需要縮小轉換(見下文)來轉換任何參數,則該程序不合格。

其中在兩種情況下需要我們[over.match.list]限定兩相過載分辨率:

  • 最初,候選功能初始化列表構造([dcl.init.list])的類T和參數列表由初始化程序列表作爲單個參數組成。
  • 如果找不到可行的初始化程序列表構造函數,則重新執行重載解析,其中候選函數是類T的所有構造函數,參數列表由初始化程序列表的元素組成。

如果初始值設定項列表中沒有元素且T有默認構造函數,則省略第一個階段。在複製列表初始化中,如果選擇了顯式構造函數,則初始化不合格。

候選人是X的構造函數。 X x{y}X x = {y}之間的唯一區別在於,如果後者選擇構造函數explicit,則初始化不合格。我們甚至沒有任何explicit構造函數,所以兩者是等價的。因此,我們列舉我們的構造函數:

  • X(Y const&)
  • X(X&&)Y::operator X()

前者的方法是直接引用結合是完全匹配。後者需要用戶定義的轉換。因此,在這種情況下,我們更喜歡X(Y const&)


注意GCC 7.1得到這個錯誤在C++ 1Z模式,所以我申請bug 80943

+0

您不是「被告知考慮轉換函數」,因爲在確定「X」的複製/移動構造函數是否爲可行候選項時,會考慮它們。他們無法與精確匹配引用綁定競爭。 –

+0

@ T.C。所以'X x {y}'會通過'Y :: operator X'將'X(X &&)'看作一個可行的候選者,而不是一個可行的候選者?換句話說,我提交的gcc錯誤是一個錯誤,但不是由於我描述的原因? – Barry

+0

非常好,是的。 –

0

我的問題是,爲什麼不定義X k = {y}以相同的方式改變其行爲?

因爲,從概念上講,= { .. }是的東西,會自動選擇「最佳」的方式,從括號初始化目標初始化,而= value也是一個初始化,但是從概念上也轉換的爲不同的值。轉換是完全對稱的:如果將查看源值以查看它是否提供了創建目標的方法,並將查看目標以查看它是否提供接受源的方式。

如果您的目標類型爲struct A { int x; },則使用= { 10 }將不會嘗試將10轉換爲A(這將失敗)。但它會尋求最好的(在他們眼中)初始化形式,這在這裏等於聚合初始化。但是,如果A不是聚合(添加構造函數),那麼它將調用構造函數,在您的情況下,它會發現容易接受的Y,而無需進行轉換。在使用= value表單時,源和目標之間沒有這種對稱性,就像使用轉換一樣。

您對轉換函數的「較少const」的懷疑是完全正確的。如果您將轉換函數設爲一個const成員,那麼它將變得不明確。