Вызов оператора преобразования вместо преобразования конструктора в С++ 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
Ответы
Ответ 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, выбирающие оператор преобразования, вряд ли являются лучшим выбором, ни в соответствии с формулировкой, ни интуицией, поэтому, если это не связано с обратной совместимостью (и даже тогда), отчет об ошибке будет уместным.