Должен ли этот код вызывать неоднозначную ошибку преобразования?

У меня есть два класса: A и B, каждый из которых определяет преобразование в B. A имеет оператор преобразования в B, B имеет конструктор из A. Не следует ли двузначный вызов static_cast<B>? Используя g++, этот код компилирует и выбирает конструктор преобразования.

#include<iostream>

using namespace std;

struct B;
struct A {
    A(const int& n) : x(n) {}
    operator B() const;         //this const doesn't change the output of this code
    int x;
};

struct B{
    B(const double& n) : x(n) {}
    B(const A& a);
    double x;
};

A::operator B() const           //this const doesn't change the output of this code
{
    cout << "called A conversion operator" << endl;
    return B(double(x));
}

B::B(const A& a)
{
    cout << "called B conversion constructor" << endl;
    x = (double) a.x;
}

int main() {
    A a(10);
    static_cast<B>(a);            // prints B conversion constructor
}

Ответ 1

Для пользовательских последовательностей преобразования; не существует приоритета между конструктором преобразования и оператором преобразования, они оба являются кандидатами;

§13.3.3.1.2/1 Пользовательские последовательности преобразований

Пользовательская последовательность преобразования состоит из начальной стандартной последовательности преобразования, за которой следует пользовательское преобразование (12.3), а затем вторая стандартная последовательность преобразования. Если определяемое пользователем преобразование задается конструктором (12.3.1), начальная стандартная последовательность преобразования преобразует тип источника в тип, требуемый аргументом конструктора. Если пользовательское преобразование задается функцией преобразования (12.3.2), начальная стандартная последовательность преобразования преобразует тип источника в параметр неявного объекта функции преобразования.

Следовательно, если преобразование было:

B b2 = a; // ambiguous?

Это может быть неоднозначно, и компиляция терпит неудачу. Clang не выполняет компиляцию, g++ принимает код и использует конструктор; демонстрационный код, VS также принимает код. VS и g++ вызывают конструктор преобразования (в соответствии с кодом OP).

С учетом опубликованного кода необходимо учитывать пользовательские последовательности преобразования (с помощью оператора конструктора и преобразования) и использования static_cast.

§5.2.9/4 Статический бросок

Выражение e может быть явно преобразовано в тип T с использованием static_cast формы static_cast<T>(e), если декларация T t(e); является корректной для некоторой изобретенной временной переменной T (8.5). Эффект такого явного преобразования такой же, как выполнение объявления и инициализации, а затем использование временной переменной в результате преобразования. Выражение e используется как glvalue тогда и только тогда, когда инициализация использует его как lvalue.

Из приведенной выше цитаты static_cast эквивалентна B temp(a); и как таковая используется прямая последовательность инициализации.

§13.3.1.3/1 Инициализация конструктором

Когда объекты типа класса инициализируются с прямой инициализацией (8.5), копируются с помощью выражения того же типа или типа производного класса (8.5) или по умолчанию (8.5), разрешают перегрузку, выбирает конструктор. Для прямой инициализации или инициализации по умолчанию все функции-кандидаты являются конструкторами класса инициализируемого объекта. Для инициализации копий функции-кандидаты являются всеми конструкторами преобразования (12.3.1) этого класса. Список аргументов - это выражение-список или выражение-присваивание инициализатора.

В общем случае (исключая любые конструкторы и операторы, помеченные как explicit и const), учитывая конструктор B(const A& a); и конструкцию a B из A, конструктор должен выиграть, так как он предлагает точное соответствие при рассмотрении лучшей жизнеспособной функции; поскольку дальнейшие неявные преобразования не нужны (§13.3; Разрешение перегрузки).


Если конструктор B(const A& a); был удален, преобразование (с помощью static_cast<> все равно будет выполнено, поскольку пользовательский оператор преобразования является кандидатом и его использование не является двусмысленным.

§13.3.1.4/1 Копирование-инициализация класса по пользовательскому преобразованию

В условиях, указанных в 8.5, как часть инициализации копии объекта типа класса, можно вызвать пользовательское преобразование, чтобы преобразовать выражение инициализатора в тип инициализированного объекта.

Цитаты взяты из черновика N4567 стандарта С++.


Было бы также поучительно вызывать пользовательскую последовательность преобразования вне только конструкции объекта, то есть вызова метода.

Учитывая список кодов (и вышеприведенные правила);

#include <iostream>
using namespace std;
struct A;
struct B {
    B() {}
    B(const A&) { cout << "called B conversion constructor" << endl; }
};
struct A {
    A() {}
    operator B() const { cout << "called A conversion operator" << endl; return B(); }
};
void func(B) {}
int main() {
    A a;
    B b1 = static_cast<B>(a); // 1. cast
    B b2 = a; // 2. copy initialise
    B b3 ( a ); // 3. direct initialise
    func(a); // 4. user defined conversion
}

Clang, g++ (demo), и VS предлагают разные результаты и, возможно, разные уровни соответствия.

  • clang не работает 2. и 4.
  • g++ принимает от 1. до 4.
  • Ошибка VS 4.

Из вышеприведенных правил с 1. по 3. все должны быть успешными, поскольку конструктор преобразования B является кандидатом и не требует дальнейших пользовательских преобразований; для этих форм используется прямое построение и инициализация копий. Чтение из стандарта (вышеприведенные выдержки, в частности, §13.3.3.1.2/1 и §13.3.1.4/1, а затем §8.5/17.6.2), 2. и 4. могут/должны потерпеть неудачу и быть неоднозначными - поскольку конструктор преобразования и оператор преобразования рассматриваются без четкого упорядочения.

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