c++ - 构造函数和转换运算符之间的重载解析

标签 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]
  • 为什么在第三种情况下甚至考虑转换运算符?为什么程序没有格式错误? [dcl.init] 状态:

  • Otherwise, if the initialization is direct-initialization, or if it is copy-initialization where the cv-unqualified version of the source type is the same class as, or a derived class of, the class of the destination, constructors are considered. The applicable constructors are enumerated (16.3.1.3), and the best one is chosen through overload resolution (16.3). The constructor so selected is called to initialize the object, with the initializer expression or expression-list as its argument(s). If no constructor applies, or the overload resolution is ambiguous, the initialization is ill-formed.


  • 为什么A的构造函数在第一种情况下是更好的选择? B的转换运算符似乎是更好的匹配,因为它不需要来自 B<int> 的隐式转换至 A<int> .
  • 为什么第一种和第二种情况产生不同的结果? C++17 发生了什么变化?

  • 附言有谁知道我在哪里可以找到详细指南,该指南描述了转换运算符如何参与重载解析,即在发生不同类型的初始化时它们与构造函数交互的方式。我知道标准提供了最准确的描述,但似乎我对标准措辞的解释与其正确含义几乎没有共同之处。某种经验法则和其他示例可能会有所帮助。

    最佳答案

    Why A's constructor is the better choice in the first case? B's conversion operator seems to be the better match since it doesn't require an implicit conversion from B<int> to A<int>.


    我相信这个选择是由于开放标准问题报告CWG 2327 :

    2327. Copy elision for direct-initialization with a conversion function

    Section: 11.6 [dcl.init]

    Status: drafting

    Submitter: Richard Smith

    Date: 2016-09-30

    Consider an example like:

    struct Cat {};
    struct Dog { operator Cat(); };
    
    Dog d;
    Cat c(d);
    

    This goes to 11.6 [dcl.init] bullet 17.6.2: [...]

    Overload resolution selects the move constructor of Cat. Initializing the Cat&& parameter of the constructor results in a temporary, per 11.6.3 [dcl.init.ref] bullet 5.2.1.2. This precludes the possitiblity of copy elision for this case.

    This seems to be an oversight in the wording change for guaranteed copy elision. We should presumably be simultaneously considering both constructors and conversion functions in this case, as we would for copy-initialization, but we'll need to make sure that doesn't introduce any novel problems or ambiguities..


    我们可能会注意到,GCC 和 Clang 分别从 7.1 和 6.0 版本(针对 C++17 语言级别)选择了转换运算符(即使问题尚未解决);在这些版本之前,GCC 和 Clang 都选择了 A<X>::A(A<U> &&) [T = X, U = int]构造函数过载。

    Why the first and second cases yield different results? What has changed in C++17?


    C++17 引入了保证复制省略,这意味着编译器在某些情况下必须省略类对象的复制和移动构造(即使它们有副作用);如果上述问题中的论点成立,则情况就是这样。

    值得注意的是,GCCClang两者都列出了 CWG 2327 的未知(/或无)状态;可能是因为问题仍处于起草阶段。

    C++17:保证复制/移动省略和用户声明的构造函数的聚合初始化
    以下程序在 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{});                                                         
    }
    
    这可能会让人感到意外。这里的核心规则是 AB是聚合(因此可以通过聚合初始化进行初始化),因为它们不包含用户提供的构造函数,只包含(显式删除)用户声明的构造函数。
    C++20 保证复制/移动省略和更严格的聚合初始化规则
    截至 P1008R1 ,已被 C++20 采用,上面的代码片段格式错误,如 AB不再是聚合体,因为它们具有用户声明的 ctors;在 P1008R1 之前,要求较弱,并且仅适用于没有用户提供的构造函数的类型。
    如果我们声明 AB要具有明确默认的定义,程序自然是格式良好的。
    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{});                                                         
    }
    

    关于c++ - 构造函数和转换运算符之间的重载解析,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62836954/

    相关文章:

    c++ - 编译琐事 : error ' ... ' does not name a type

    c++ - 在需要访问私有(private)结构的 .cpp 文件中定义静态成员

    c++ - 在 for(-each) 自动循环中删除项目

    c# - 避免泛型类型的模糊调用错误

    c++ - 函数模板和常规重载

    c++ - 在哪里定义了名称查找规则来查找最直接的名称声明?

    C++ 派生类在初始化之前调用基类的方法

    更改 const 对象 - 没有警告?另外,在什么情况下是 UB?

    c++ - C++ 中一直存在纯虚方法吗?

    c# - 为什么 C# 编译器使用隐式运算符返回的值的父类型来调用重载运算符