Разрешение перегрузки между конструкторами и операторами преобразования - PullRequest
6 голосов
/ 10 июля 2020

У меня есть пара вопросов, связанных с разрешением перегрузки в 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. Тип источника является тем же классом, что и класс назначения, или производным от него, рассматриваются конструкторы . Применимые конструкторы перечислены (16.3.1.3), и лучший из них выбирается посредством разрешения перегрузки (16.3). Выбранный таким образом конструктор вызывается для инициализации объекта с выражением инициализатора или списком выражений в качестве аргумента (ов). Если конструктор не применяется или разрешение перегрузки неоднозначно, инициализация сформирована неправильно.
    • Почему конструктор A является лучшим выбором в первый случай? Оператор преобразования? B кажется более подходящим, поскольку он не требует неявного преобразования из B<int> в A<int>.
    • Почему первый и второй случаи дают разные результаты? Что изменилось в C ++ 17?

    PS Кто-нибудь знает, где я могу найти подробное руководство, в котором описывается, как операторы преобразования участвуют в разрешении перегрузки, т.е. как они взаимодействуют с конструкторами, когда они разные. типы инициализации. Я знаю, что стандарт обеспечивает наиболее точное описание, но мне кажется, что моя интерпретация стандартной формулировки имеет мало общего с ее правильным значением. Может быть полезно какое-то практическое правило и дополнительные примеры.

1 Ответ

2 голосов
/ 10 июля 2020

Почему конструктор A - лучший выбор в первом случае? Оператор преобразования B кажется более подходящим, поскольку он не требует неявного преобразования из B<int> в A<int>.

Я считаю, что этот выбор обусловлен открытым стандартом отчет о проблеме CWG 2327 :

2327. Копирование разрешения для прямой инициализации с функцией преобразования

Раздел: 11.6 [dcl.init]

Статус: черновик

Отправитель: Ричард Смит

Дата: 2016-09-30

Рассмотрим пример, например:

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

Dog d;
Cat c(d);

Это относится к 11.6 [dcl.init], пулю 17.6.2: [...]

Разрешение перегрузки выбирает конструктор перемещения Cat. Инициализация параметра Cat&& конструктора приводит к временному, согласно 11.6.3 [dcl.init.ref] подпункту 5.2.1.2. Это исключает возможность исключения копии в данном случае.

Это, по-видимому, недосмотр в изменении формулировки для гарантированного исключения копии. Предположительно, мы должны быть одновременно с учетом как конструкторов, так и функций преобразования в этом случае, как и для инициализации копирования , но нам нужно убедиться, что это не создает каких-либо новых проблем или двусмысленностей.

Можно отметить, что и G CC, и Clang выбирают оператор преобразования (хотя проблема еще не решена) из версий 7.1 и 6.0 соответственно (для уровня языка C ++ 17); до этих выпусков и G CC, и Clang выбирали перегрузку A<X>::A(A<U> &&) [T = X, U = int] ctor.

Почему первый и второй случаи дают разные результаты? Что изменилось в C ++ 17?

В C ++ 17 введено гарантированное исключение копирования, что означает, что компилятор должен опускать копирование и перемещение объектов класса (даже если они иметь побочные эффекты) при определенных обстоятельствах; если аргумент в вышеупомянутом вопросе верен, это такое обстоятельство.

Примечательно, что G CC и Clang оба перечисляют неизвестный (/ или нулевой) статус 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{});                                                         
}

который может стать сюрпризом. Основное правило здесь состоит в том, что и A, и B являются агрегатами (и, таким образом, могут быть инициализированы посредством агрегатной инициализации), поскольку они не содержат только конструкторы user- при условии (явно удалены) user- объявлено единиц.

C ++ 20 гарантированное исключение копирования / перемещения и более строгие правила для агрегированной инициализации

Начиная с P1008R1 , который имеет был принят для C ++ 20, приведенный выше фрагмент неверно сформирован, поскольку A и B больше не являются агрегатами, поскольку для них объявлены user- ctors; до P1008R1 требования были слабее, и только для типов не было предоставлено user- ctors.

Если мы объявляем 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{});                                                         
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...