簡體   English   中英

顯式構造函數和嵌套的初始化程序列表

[英]Explicit constructors and nested initializer lists

以下代碼可使用大多數現代C ++ 11兼容編譯器(GCC> = 5.x,Clang,ICC,MSVC)成功編譯。

#include <string>

struct A
{
        explicit A(const char *) {}
        A(std::string) {}
};

struct B
{
        B(A) {}
        B(B &) = delete;
};

int main( void )
{
        B b1({{{"test"}}});
}

但是,為什么要首先編譯它?列出的編譯器如何解釋該代碼?

為什么MSVC能夠在沒有B(B &) = delete;情況下進行編譯B(B &) = delete; ,但是其他3個編譯器都需要嗎?

當我刪除復制構造函數的另一個簽名時,為什么它在MSVC之外的所有編譯器中都失敗,例如B(const B &) = delete;

編譯器是否都選擇相同的構造函數?

為什么Clang發出以下警告?

17 : <source>:17:16: warning: braces around scalar initializer [-Wbraced-scalar-init]
        B b1({{{"test"}}});

與其解釋編譯器的行為,不如解釋該標准的含義。

主要例子

為了從{{{"test"}}}直接初始化b1 ,重載解析適用於選擇B的最佳構造函數。 因為沒有從{{{"test"}}}B&隱式轉換(列表初始化器不是左值),所以構造函數B(B&)不可行。 然后,我們專注於構造函數B(A) ,並檢查它是否可行。

為了確定從{{{"test"}}}A的隱式轉換順序(為簡化起見,我將使用符號{{{"test"}}} -> A ),重載分辨率適用於選擇A的最佳構造函數,因此我們需要根據[over.match.list]比較{{"test"}} -> const char*{{"test"}} -> std::string (注意括號的最外層被刪除)。 ] / 1

當非聚合類類型T的對象被列表初始化,使得[dcl.init.list]指定根據本小節中的規則執行重載解析時,重載解析在兩個階段中選擇構造函數:

  • 最初,候選函數是類T的初始化器列表構造函數([dcl.init.list])。

  • 如果找不到可行的初始化器列表構造函數,則再次執行重載解析,其中候選函數是T類的所有構造器,並且參數列表由初始化器列表的元素組成。

...在復制列表初始化中,如果選擇了顯式構造函數,則初始化格式錯誤。

請注意,無論指定符explicit是什么,這里都考慮所有構造explicit

根據[over.ics.list] / 10[over.ics.list] / 11, {{"test"}} -> const char*不存在:

否則,如果參數類型不是類:

  • 如果初始化列表中有一個本身不是初始化列表的元素,則 ...

  • 如果初始化列表沒有元素...

除上面列舉的情況外,在所有情況下均無法進行轉換。

要確定{{"test"}} -> std::string ,將執行相同的過程,並且重載解析選擇std::string的構造函數,該構造函數采用const char*類型的參數。

結果, {{{"test"}}} -> A通過選擇構造函數A(std::string)


變化

如果explicit刪除了該怎么辦?

該過程不會改變。 GCC將選擇構造函數A(const char*)而Clang將選擇構造函數A(std::string) 我認為這是GCC的錯誤。

如果b1的初始化程序中只有兩層大括號怎么辦?

注意{{"test"}} -> const char*不存在,但是{"test"} -> const char*存在。 因此,如果b1的初始化程序中只有兩層括號,則選擇構造函數A(const char*) ,因為{"test"} -> const char*{"test"} -> std::string 結果,在復制列表初始化(從{"test"}初始化構造函數B(A)中的參數A中選擇了一個顯式構造函數,然后該程序格式錯誤。

如果聲明了構造函數B(const B&) ,該怎么辦?

請注意,如果刪除了B(B&)的聲明,也會發生這種情況。 這次,我們需要比較{{{"test"}}} -> A{{{"test"}}} -> const B&{{{"test"}}} -> const B

為了確定{{{"test"}}} -> const B ,采用了上述過程。 我們需要比較{{"test"}} -> A{{"test"}} -> const B& 注意{{"test"}} -> const B&根據[over.best.ics] / 4不存在:

但是,如果目標是

—構造函數的第一個參數,或者

—用戶定義的轉換函數的隱式對象參數

並且構造函數或用戶定義的轉換函數是

— [over.match.ctor],當參數是類復制初始化的第二步中的臨時參數時,

— [over.match.copy],[over.match.conv]或[over.match.ref](在所有情況下),或

[over.match.list]的第二階段,當初始值設定項列表恰好具有一個本身就是初始值設定項列表的元素,並且目標是類X的構造函數的第一個參數,並且轉換為X或對簡歷 X

不考慮用戶定義的轉換順序。

為了確定{{"test"}} -> A ,再次采用上述過程。 這幾乎與我們在前面小節中討論的情況相同。 結果,選擇了構造函數A(const char*) 請注意,此處選擇了構造函數來確定{{{"test"}}} -> const B ,但實際上並不適用。 盡管構造函數是顯式的,但這是允許的。

結果, {{{"test"}}} -> const B通過選擇構造函數B(A) ,然后選擇構造函數A(const char*) 現在{{{"test"}}} -> A{{{"test"}}} -> const B都是用戶定義的轉換序列,兩者都不比另一個更好,因此b1的初始化是模棱兩可的。

如果括號被大括號代替怎么辦?

根據[over.best.ics] / 4 (在上一小節中用引號引用),不考慮用戶定義的轉換{{{"test"}}} -> const B& 因此,即使聲明了構造函數B(const B&) ,結果也與主要示例相同。

B b1({{{"test"}}}); 就像B b1(A{std::string{const char*[1]{"test"}}});

16.3.3.1.5列表初始化序列[over.ics.list]

4否則,如果參數類型為字符數組133,並且初始化程序列表包含單個元素,該元素為適當類型的字符串文字(11.6.2),則隱式轉換序列為身份轉換。

然后編譯器嘗試所有可能的隱式轉換。 例如,如果我們具有帶有以下構造函數的C類:

#include <string>

struct C
{
    template<typename T, size_t N>     C(const T* (&&) [N]) {}
    template<typename T, size_t N>     C(const T  (&&) [N]) {}
    template<typename T=char>         C(const T* (&&)) {}
    template<typename T=char>          C(std::initializer_list<char>&&) {}
};

struct A
{
    explicit A(const char *) {}

    A(C ) {}
};

struct B
{
    B(A) {}
    B(B &) = delete;
};

int main( void )
{
    const char* p{"test"};
    const char p2[5]{"test"};

    B b1({{{"test"}}});
}

clang 5.0.0編譯器無法決定使用哪個,並且失敗:

29 : <source>:29:11: error: call to constructor of 'C' is ambiguous
    B b1({{{"test"}}});
          ^~~~~~~~~~
5 : <source>:5:40: note: candidate constructor [with T = char, N = 1]
    template<typename T, size_t N>     C(const T* (&&) [N]) {}
                                       ^
6 : <source>:6:40: note: candidate constructor [with T = const char *, N = 1]
    template<typename T, size_t N>     C(const T  (&&) [N]) {}
                                       ^
7 : <source>:7:39: note: candidate constructor [with T = char]
    template<typename T=char>         C(const T* (&&)) {}
                                      ^
15 : <source>:15:9: note: passing argument to parameter here
    A(C ) {}
        ^

但是,如果我們只留下一個非初始化列表構造函數,則代碼可以正常編譯。

GCC 7.2僅選擇C(const T* (&&)) {}並進行編譯。 如果不可用,則使用C(const T* (&&) [N])

MSVC失敗,並顯示:

29 : <source>(29): error C2664: 'B::B(B &)': cannot convert argument 1 from 'initializer list' to 'A'

(編輯,謝謝@dyp)

這是部分答案和推測,它解釋了我如何解釋發生的事情,而不是作為一名編譯專家並且沒有太多C ++ Guru。

首先,我將具有一些直覺和常識。 顯然,最后發生的事情是B::B(A) ,因為這是B b1唯一可用的構造函數(顯然它不能是B::B(B&&)因為至少定義了一個復制構造函數,所以B::B(B&&)未為我們隱式定義)。 同樣,A或B的第一個構造不能是A::A(const char*)因為它是顯式的,因此必須對A::A(std::string)進行一些使用。 另外,最里面引用的文本是const char[5] 因此,我猜第一個最里面的構造是const char* 然后是字符串構造: std::string::string(const char *) 還有另外一種花括號結構,我猜是A::A(A&&) (或者也許是A::A(A&)嗎?)。 因此,總結我的直覺猜測,構造的順序應為:

  1. const char*
  2. 一個std::string (實際上是std::basic_string<whatever>
  3. 一個A

然后我將其放在GodBolt上 ,以GCC作為第一個示例。 (或者,您可以在保留匯編語言輸出的同時自行編譯,然后將其傳遞給c++filt以使其更具可讀性)。 以下是所有專門提及C ++代碼的行:

call   4006a0 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)@plt>
call   400858 <A::A(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)>
call   400868 <B::B(A)>
call   400680 <std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string()@plt>
call   400690 <std::allocator<char>::~allocator()@plt>
call   400690 <std::allocator<char>::~allocator()@plt>

因此,似乎我們看到的適當可行的構造的順序是:

(看不到1.)2. std::basic_string::basic_string(const char* /* ignoring the allocator */) 3. A::A(std::string) 4. B::B(A)

使用clang 5.0.0時,結果與IIANM相似,對於MSVC來說也是如此-誰知道? 也許是個錯誤? 他們有時在完全支持語言標准方面有些不知所措。 抱歉,就像我說的-部分答案。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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