Bay*_*ita 6 c++ language-lawyer overload-resolution
我有几个与 C++ 中的重载解析相关的问题。考虑这个例子:
extern "C" int printf (const char*, ...);                                       
                                                                                
struct X {};                                                                    
                                                                                
template <typename T>                                                           
struct A                                                                        
{                                                                               
    A() = default;                                                              
                                                                                
    template <typename U>                                                       
    A(A<U>&&)                                                                   
    {printf("%s \n", __PRETTY_FUNCTION__);}                                     
};                                                                              
                                                                                
template <typename T>                                                           
struct B : A<T>                                                                 
{                                                                               
    B() = default;                                                              
                                                                                
    template <typename U>                                                       
    operator A<U>()                                                             
    {printf("%s \n", __PRETTY_FUNCTION__); return {};}                          
};                                                                              
                                                                                
int main ()                                                                     
{                                                                               
    A<X> a1 (B<int>{});                                                         
} 
如果我用 编译它g++ -std=c++11 a.cpp,A的构造函数将被调用:
A<T>::A(A<U>&&) [with U = int; T = X] 
如果我用 编译程序g++ -std=c++17 a.cpp,它会产生
B<T>::operator A<U>() [with U = X; T = int]
如果我注释掉A(A<U>&&)并再次用 编译它g++ -std=c++11 a.cpp,转换运算符将被调用:
B<T>::operator A<U>() [with U = X; T = int]
否则,如果初始化是直接初始化,或者是复制初始化,其中源类型的 cv-unqualified 版本与目标类相同,或者是目标类的派生类,则考虑构造函数。枚举适用的构造函数(16.3.1.3),通过重载决议(16.3)选择最好的构造函数。如此选择的构造函数被调用以初始化对象,并将初始化表达式或表达式列表作为其参数。如果没有构造函数适用,或者重载决议不明确,则初始化格式错误。
A在第一种情况下构造函数是更好的选择?B的转换运算符似乎是更好的匹配,因为它不需要从B<int>到的隐式转换A<int>。PS 有谁知道我在哪里可以找到详细的指南,该指南描述了转换运算符如何参与重载解析,即当不同类型的初始化发生时它们与构造函数交互的方式。我知道标准提供了最准确的描述,但似乎我对标准措辞的解释与其正确含义几乎没有共同之处。某种经验法则和其他示例可能会有所帮助。
为什么
A的构造函数在第一种情况下是更好的选择?B的转换运算符似乎是更好的匹配,因为它不需要从B<int>到 的隐式转换A<int>。
我相信这个选择是由于开放标准问题报告CWG 2327:
2327. 使用转换函数进行直接初始化的复制省略
节: 11.6 [dcl.init]
状态:起草中
提交者:理查德·史密斯
日期: 2016-09-30
考虑一个例子:
Run Code Online (Sandbox Code Playgroud)struct Cat {}; struct Dog { operator Cat(); }; Dog d; Cat c(d);这转到 11.6 [dcl.init] 项目符号 17.6.2:[...]
重载决策选择 的移动构造函数
Cat。根据 11.6.3 [dcl.init.ref] 项目符号 5.2.1.2,初始化Cat&&构造函数的参数会产生临时结果。这排除了这种情况下复制省略的可能性。这似乎是对保证复制省略的措辞更改的疏忽。在这种情况下,我们应该同时考虑构造函数和转换函数,就像我们对复制初始化一样,但我们需要确保这不会引入任何新的问题或歧义。
我们可能会注意到,GCC 和 Clang 分别从版本 7.1 和 6.0(对于 C++17 语言级别)选择转换运算符(即使问题尚未解决 DR);在这些版本之前,GCC 和 Clang 都选择了A<X>::A(A<U> &&) [T = X, U = int]ctor 重载。
为什么第一种情况和第二种情况会产生不同的结果?C++17 发生了什么变化?
C++17 引入了保证复制省略,这意味着编译器在某些情况下必须省略类对象的复制和移动构造(即使它们有副作用);如果上述问题的论点成立,则属于这种情况。
值得注意的是,GCC和Clang都列出了 CWG 2327 的未知(/或无)状态;可能是因为问题仍然处于“Drafting”状态。
以下程序在 C++17 中格式良好:
struct A {                                                                               
    A() = delete;                                                            
    A(const A&) = delete;         
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;                                 
};                                                                              
                                                                                                                                  
struct B {                                                                               
    B() = delete;                                                         
    B(const B&) = delete;         
    B(B&&) = delete;
    B& operator=(const B&) = delete;
    B& operator=(B&&) = delete;  
                                                    
    operator A() { return {}; }                          
};                                                                              
                                                                                
int main ()                                                                     
{   
    //A a;   // error; default initialization (deleted ctor)
    A a{}; // OK before C++20: aggregate initialization
    
    // OK int C++17 but not C++20: 
    // guaranteed copy/move elision using aggr. initialization
    // in user defined B to A conversion function.
    A a1 (B{});                                                         
}
这可能会让人感到惊讶。这里的核心规则是 和A都是B聚合(因此可以通过聚合初始化来初始化),因为它们不包含用户提供的构造函数,仅包含(显式删除的)用户声明的构造函数。
从C++20 采用的P1008R1A开始,上面的代码片段格式不正确,因为和B不再是聚合,因为它们具有用户声明的构造函数;在 P1008R1 之前,要求较弱,并且仅适用于没有用户提供的类型的类型。
如果我们声明A和B具有显式默认定义,则程序自然是格式良好的。
struct A {                                                                               
    A() = default;                                                            
    A(const A&) = delete;         
    A(A&&) = delete;
    A& operator=(const A&) = delete;
    A& operator=(A&&) = delete;                                 
};                                                                              
                                                                                                                                  
struct B {                                                                               
    B() = default;                                                         
    B(const B&) = delete;         
    B(B&&) = delete;
    B& operator=(const B&) = delete;
    B& operator=(B&&) = delete;  
                                                    
    operator A() { return {}; }                          
};                                                                              
                                                                                
int main ()                                                                     
{   
    // OK: guaranteed copy/move elision.
    A a1 (B{});                                                         
}