Подтвердить что ты не робот

Вызов оператора преобразования вместо преобразования конструктора в С++ 17 во время разрешения перегрузки

Рассмотрим следующие два класса:

#define PRETTY(x) (std::cout << __PRETTY_FUNCTION__ << " : " << (x) << '\n')

struct D;

struct C {
    C() { PRETTY(this);}
    C(const C&) { PRETTY(this);}
    C(const D&) { PRETTY(this);}
};

struct D {
    D() { PRETTY(this);}
    operator C() { PRETTY(this); return C();}
};

Мы заинтересованы в разрешении перегрузки между двумя конструкторами:

C::C(const C&);
C::C(const D&);

Этот код работает как ожидалось:

void f(const C& c){ PRETTY(&c);}
void f(const D& d){ PRETTY(&d);}
/*--------*/
D d;
f(d); //calls void f(const D& d)

так как void f(const D& d) является лучшим совпадением.

Но:

D d;
C c(d);

(как вы можете видеть здесь)

вызывает D::operator C() при компиляции с std=c++17 и вызывает C::C(const D&) с std=c++14 как для clang, так и для gcc HEAD. Более того, это поведение недавно изменилось: с clang 5, C::C(const D&) вызывается с std=c++17 и std=c++14.

Какое здесь правильное поведение?

Предварительное чтение стандарта (последний проект N4687):

C c(d) - это прямая инициализация, которая не является копией elision ([dcl.init]/17.6.1). [dcl.init]/17.6.2 сообщает нам, что соответствующие конструкторы перечислены и что лучший выбирается с помощью разрешения перегрузки. [over.match.ctor] сообщает, что применимые конструкторы в этом случае являются всеми конструкторами.

В этом случае: C(), C(const C&) и C(const D&) (без перемещения ctor). C() явно нежизнеспособен и, следовательно, отбрасывается из набора перегрузки. ([Over.match.viable])

Конструкторы не имеют неявного параметра объекта, поэтому C(const C&) и C(const D&) берут ровно один параметр. ([Over.match.funcs]/2)

Теперь переходим к [over.match.best]. Здесь мы находим, что нам нужно определить, какие из этих двух неявных преобразований (ICS) лучше. ICS C(const D&) включает только стандартную последовательность преобразования, но ICS C(const C&) включает пользовательскую последовательность преобразования.

Поэтому C(const D&) следует выбирать вместо C(const C&).


Интересно, что эти две модификации приводят к тому, что конструктор "right" назовем:

operator C() { /* */ } в operator C() const { /* */ }

или

C(const D&) { /* */ } в C(D&) { /* */ }

Это то, что произойдет (я думаю) в случае инициализации копирования, когда определяемые пользователем преобразования и конструкторы преобразования подвержены перегрузке разрешение.


Поскольку Коломбо рекомендует, чтобы я подал отчет об ошибке с gcc и clang

gcc bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82840

clang bug https://bugs.llvm.org/show_bug.cgi?id=35207

4b9b3361

Ответ 1

Основная проблема 243 (17 лет!):

Существует серьезная проблема с определением перегрузки разрешающая способность. Рассмотрим этот пример:

struct B;
struct A {
    A(B);
};
struct B {
    operator A();
} b;
int main() {
    (void)A(b);
} 

Это в значительной степени определение "двусмысленно", правильно? Вы хотите преобразовать B в A, и есть два одинаково хороших способа делая это: конструктор A, который принимает B, и преобразование функция B, которая возвращает A.

Что мы обнаруживаем, когда мы отслеживаем это через стандарт, к сожалению, заключается в том, что конструктор предпочитает преобразование функция. Определение прямой инициализации (в скобках форма) класса рассматривает только конструкторы этого класса. В этом case, конструкторы являются конструкторами A(B) и (неявно сгенерированный) конструктор копирования A(const A&). Вот как они ранжируются по совпадению аргументов:

  • A(B): точное совпадение (требуется B, иметь B)
  • A(const A&): пользовательское преобразование (B::operator A используется для преобразования B в A)

Иными словами, функция преобразования считается рассмотренной, но она работает с эффект, гандикап одного определенного пользователем преобразования. Чтобы по-разному, эта проблема - проблема взвешивания, а не проблема что некоторые пути преобразования не рассматриваются. [...]

Примечания с 10/01 заседания:

Оказывается, существует существующая практика в обоих направлениях по этой проблеме, поэтому не ясно, что он "сломан". Есть некоторая причина чувствовать что то, что выглядит как "вызов конструктора", должно вызвать конструктор, если это возможно, а не функцию преобразования. CWG решил оставить его в покое.

Кажется, ты прав. Более того, Clang и GCC, выбирающие оператор преобразования, вряд ли являются лучшим выбором, ни в соответствии с формулировкой, ни интуицией, поэтому, если это не связано с обратной совместимостью (и даже тогда), отчет об ошибке будет уместным.