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

Overload resolution between constructors and conversion operators

我有几个与 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]

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.

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

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 语言级别)中选择了转换运算符(即使问题尚未解决 DR);在这些版本之前,GCC 和 Clang 都选择了 A<X>::A(A<U> &&) [T = X, U = int] ctor 重载。

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

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


值得注意的是,GCC and Clang 都列出了 CWG 2327 的未知(/或 none)状态;可能因为问题是它仍处于状态 起草.


C++17:保证copy/move用户声明的构造函数的省略和聚合初始化

以下程序在 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 都是聚合(因此可以通过聚合初始化的方式进行初始化),因为它们不包含 user-provided构造函数,仅(明确删除)用户声明

C++20 保证 copy/move 聚合初始化的省略和更严格的规则

从已被 C++20 采用的 P1008R1 开始,上面的代码片段格式不正确,因为 AB 不再像以前那样聚合用户-声明 ctors;在 P1008R1 之前,要求较弱,并且仅针对没有用户提供 ctors 的类型。

如果我们声明 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{});                                                         
}