Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overload resolution between constructors and conversion operators

I have a couple of questions related to overload resolution in C++. Consider this example:

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>{});                                                         
} 

If I compile it with g++ -std=c++11 a.cpp, the A's constructor will get called:

A<T>::A(A<U>&&) [with U = int; T = X] 

If I compile program with g++ -std=c++17 a.cpp, it will produce

B<T>::operator A<U>() [with U = X; T = int]

If I comment A(A<U>&&) out and once again compile it with g++ -std=c++11 a.cpp, the conversion operator will be called:

B<T>::operator A<U>() [with U = X; T = int]
  • Why is the conversion operator even considered in the third case? Why is the program not ill-formed? [dcl.init] states:

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.

  • 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>.
  • Why the first and second cases yield different results? What has changed in C++17?

P.S. Does anyone know where I can find a detailed guide that describes how conversion operators participate in overload resolution, i.e., the ways they interact with the constructors when different types of initialization take place. I know that the standard provides the most accurate description, but it seems that my interpretation of the standard wording has little in common with its correct meaning. Some kind of rule of thumb and additional examples might be helpful.

like image 209
Baykov Nikita Avatar asked Jul 10 '20 14:07

Baykov Nikita


People also ask

What is the overload resolution?

The process of selecting the most appropriate overloaded function or operator is called overload resolution. Suppose that f is an overloaded function name. When you call the overloaded function f() , the compiler creates a set of candidate functions.

What is operator overloading explain conversion?

The operator overloading defines a type conversion operator that can be used to produce an int type from a Counter object. This operator will be used whenever an implicit or explict conversion of a Counter object to an int is required. Notice that constructors also play a role in type conversion.

What is overloading constructor in C++?

Constructor overloading means having more than one constructor with the same name. Constructors are methods invoked when an object is created. You have to use the same name for all the constructors which is the class name. This is done by declaration the constructor with a different number of arguments.

When compiler decides binding of an overloaded member then it is called?

Method Overloading and Operator Overloading are examples of the same. It is known as Early Binding because the compiler is aware of the functions with same name and also which overloaded function is tobe called is known at compile time.


1 Answers

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>.

I believe this choice is due to the open standard issue report 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..

We may note that it both GCC and Clang picks the conversion operator (even though the issue is not yet a resolved DR) from versions 7.1 and 6.0, respectively (for C++17 language level); prior to these releases both GCC and Clang chose the A<X>::A(A<U> &&) [T = X, U = int] ctor overload.

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

C++17 introduced guaranteed copy elision, meaning the compiler must omit the copy and move construction of class objects (even if they have side effects) under certain circumstances; if the argument in the issue above hold, this is such a circumstance.


Notably, GCC and Clang both lists unknown (/or none) status of CWG 2327; possibly as the issue is it still in status Drafting.


C++17: guaranteed copy/move elision & aggregate initialization of user-declared constructors

The following program is well-formed in 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{});                                                         
}

which may come as a surprise. The core rule here is that both A and B are aggregates (and may thus be initialized by means of aggregate initialization) as they do not contain user-provided constructors, only (explicitly-deleted) user-declared ones.

C++20 guaranteed copy/move elision & stricter rules for aggregate initialization

As of P1008R1, which has been adopted for C++20, the snippet above is ill-formed, as A and B are no longer aggregates as they have user-declared ctors; prior to P1008R1 the requirement were weaker, and only for types not to have user-provided ctors.

If we declare A and B to have explicitly-defaulted definitions, the program is naturally well-formed.

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{});                                                         
}
like image 137
dfrib Avatar answered Oct 20 '22 19:10

dfrib