Интересно, какой компилятор соответствует стандарту, я использую следующий код
#include <iostream>
#include <string>
#include <memory>
#include <vector>
class AbstractBase
{
public:
virtual ~AbstractBase() {};
virtual std::string get_name() = 0;
virtual int get_number() = 0;
};
class BaseImpl : public AbstractBase
{
public:
BaseImpl() = delete; //(a)
//BaseImpl(BaseImpl&) = delete; //(b)
BaseImpl(const std::vector<std::string>& name_) : name(name_) {}
std::string get_name() override {return name.empty() ? std::string("empty") : name.front();}
private:
std::vector<std::string> name{};
};
class impl : public BaseImpl
{
public:
impl() : BaseImpl({}) {}
int get_number() override {return 42;}
};
int main()
{
std::unique_ptr<AbstractBase> intance = std::make_unique<impl>();
std::cout << intance->get_name() << " " << intance->get_number() << "\n";
return 0;
}
msvc и clang выдают ошибку компилятора, в то время как gcc работает с этим кодом. Ссылка на богобоязненность
https://godbolt.org/z/ETYGn5T1h
Если один явный удалить ctor копии BaseImpl (раскомментировать (b)) все три компилятора в порядке. Кроме того, если не удалить явно (а) стандартный ctor (тогда стандартный ctor не создается, потому что существует определяемый пользователем ctor), все компиляторы в порядке.
По-видимому, clang и msvc считают, что с помощью : BaseImpl({})
они могут либо вызвать определяемый пользователем ctor, либо косвенно через ctor по умолчанию неявный ctor копирования. Однако, поскольку ctor по умолчанию удален, этой двусмысленности вообще не должно быть. Однако, явно удаляя ctor по умолчанию, компиляторы сначала предполагают, что для BaseImpl существует ctor по умолчанию, и выдают ошибку, прежде чем проверять, удален ли он.
Теперь мне интересно, соответствует ли это поведение стандарту.
Обновлено: я прикрепляю вывод компилятора на случай, если кто-то не хочет или не может щелкнуть ссылку на проводник компилятора:
не уверен, но я думаю, вы неправильно понимаете, что =delete
не приводит к полному игнорированию функции. Сначала происходит разрешение перегрузки godbolt.org/z/6nhvc368d
@ 463035818_is_not_a_number да, это имеет смысл. Но здесь компилятор видит удаленный стандартный конструктор и говорит, хорошо, есть способ неявно преобразовать {} в baseimpl, и я думаю, что он должен понять, что этот способ недействителен из-за удаления, прежде чем он выдаст ошибку из-за двусмысленности.
Комментарий @aschepler поставил под сомнение этот вывод. См. там подробности или принять участие, если вы языковой юрист. Я посмотрю повнимательнее сегодня вечером, а затем обновлю этот ответ.
GCC верен совершенно неочевидным образом.
Сообщение об ошибке clang кажется довольно ясным:
<source>:28:14: error: call to constructor of 'BaseImpl' is ambiguous
impl() : BaseImpl({}) {}
^ ~~
<source>:14:7: note: candidate constructor (the implicit move constructor)
class BaseImpl : public AbstractBase
^
<source>:14:7: note: candidate constructor (the implicit copy constructor)
<source>:19:5: note: candidate constructor
BaseImpl(const std::vector<std::string>& name_) : name(name_) {}
^
1 error generated.
Compiler returned: 1
Для вызова BaseImpl({})
доступны три конструктора — сгенерированные конструкторы копирования и перемещения и векторный конструктор. Clang не знает, какой из них выбрать.
Ошибка MSVC немного менее проста:
x64 msvc v19.latest (Editor #1)
x64 msvc v19.latest
x64 msvc v19.latest
/std:c++20
123
<Compilation failed>
# For more information see the output window
x64 msvc v19.latest - 2681ms
Output of x64 msvc v19.latest (Compiler #1)
example.cpp
<source>(28): error C2259: 'BaseImpl': cannot instantiate abstract class
<source>(14): note: see declaration of 'BaseImpl'
<source>(28): note: due to following members:
<source>(28): note: 'int AbstractBase::get_number(void)': is abstract
<source>(11): note: see declaration of 'AbstractBase::get_number'
Compiler returned: 2
Что здесь происходит, так это то, что MSVC пытается вызвать конструктор копирования или перемещения, что для этого он пытается создать временный тип BaseImpl
из инициализатора {}
, и это терпит неудачу, потому что BaseImpl
является абстрактным, прежде чем произойдет сбой, потому что конструктор по умолчанию удален ( так что вы не получите сообщение об ошибке об этом).
GCC не учитывает элементы копирования и перемещения, даже если они явно добавлены, поэтому просто строит вектор и компилируется нормально.
Давайте углубимся в стандарт. В частности, давайте посмотрим на [dcl.init.general]. Я буду опускать несоответствующие части стандартного языка и обозначать их с помощью (...).
Во-первых, обратите внимание, что согласно [dcl.init.general] (15)
15 Инициализация, которая происходит
(15.1) — для инициализатора, который представляет собой список-выражений в скобках или список-инициалов в фигурных скобках,
(...)
называется прямой инициализацией.
В [dcl.init.general] (16) следует длинный список условий того, что происходит при инициализации. Здесь уместно (16.6)
(16.6) — В противном случае, если целевой тип является типом класса (возможно, cv-квалифицированным):
(...)
(16.6.2) — В противном случае, если инициализация является прямой инициализацией или если это инициализация копированием, где версия исходного типа без уточнения cv является тем же классом или производным классом от класса назначения , конструкторы считаются. Перечисляются применимые конструкторы (12.4.2.4), и лучший из них выбирается с помощью разрешения перегрузки (12.4). Затем:
(16.6.2.1) — Если разрешение перегрузки успешно, вызывается выбранный конструктор для инициализации объекта с выражением инициализатора или списком выражений в качестве его аргумента (ов).
(...)
(16.6.2.3) — В противном случае инициализация имеет неправильный формат.
Это сводится к следующему: мы ищем все применимые конструкторы и пытаемся выбрать правильный. Если это работает, мы используем это, иначе это ошибка.
Итак, давайте посмотрим на разрешение перегрузки в [over.match.ctor]. Здесь говорится, что
1 Когда объекты типа класса инициализируются напрямую (9.4), (...), разрешение перегрузки выбирает конструктор. Для прямой инициализации или инициализации по умолчанию, которая не находится в контексте инициализации копированием, все функции-кандидаты являются конструкторами класса инициализируемого объекта. (...). Список аргументов — это список-выражений или выражение-присваивания инициализатора.
Таким образом, наш набор функций-кандидатов — это сгенерированные векторы копирования и перемещения, а также векторный фактор. Следующий шаг — проверка того, какие из них жизнеспособны в соответствии с [over.match.viable]. Это означает сначала проверку того, что количество аргументов в вызове соответствует функциям-кандидатам (верно для всех кандидатов), а затем
4 В-третьих, чтобы F была жизнеспособной функцией, для каждого аргумента должна существовать последовательность неявного преобразования (12.4.4.2), которая преобразует этот аргумент в соответствующий параметр F. Если параметр имеет ссылочный тип, последовательность неявного преобразования включает операция привязки ссылки, и тот факт, что ссылка lvalue на неконстантное значение не может быть привязана к rvalue и что ссылка rvalue не может быть привязана к lvalue, может повлиять на жизнеспособность функции (см. 12.4.4.2.5).
Последовательность неявного преобразования, согласно [over.best.ics.general],
3 Правильно сформированная последовательность неявного преобразования может быть одной из следующих форм: > (3.1) — стандартная последовательность преобразования (12.4.4.2.2),
(3.2) — определяемая пользователем последовательность преобразования (12.4.4.2.3), или
(3.3) — последовательность преобразования многоточия (12.4.4.2.4).
где стандартная последовательность преобразования в основном связана с такими вещами, как int в long, lvalue в rvalue, ref в const ref и т. д. Здесь нас интересуют определяемые пользователем последовательности преобразования, которые
1 Определяемая пользователем последовательность преобразования состоит из исходной стандартной последовательности преобразования, за которой следует определяемое пользователем преобразование (11.4.8), за которым следует вторая стандартная последовательность преобразования. Если определяемое пользователем преобразование указано конструктором (11.4.8.2), начальная стандартная последовательность преобразования преобразует исходный тип в тип первого параметра этого конструктора. (...)
2 Вторая стандартная последовательность преобразования преобразует результат определяемого пользователем преобразования в целевой тип для последовательности; любое эталонное связывание включается во вторую стандартную последовательность преобразования. (...).
(...)
Определенно существует определенная пользователем последовательность преобразования из {}
в std::vector<std::string>
. Поскольку конструктор BaseImpl
по умолчанию удален, пользовательская последовательность преобразования из {}
в BaseImpl
отсутствует; для этого потребуются два определяемых пользователем преобразования: одно в std::vector<std::string>
, а другое в BaseImpl
.
Таким образом, из трех конструкторов-кандидатов только конструктор std::vector<std::string>
является жизнеспособным и подходит для разрешения перегрузки, и его следует выбрать. GCC делает это, и если я не ошибся в своем анализе, MSVC и clang содержат ошибку.
Спасибо, это очень полезно и соответствует тому, что я подозревал, но подозревать не значит знать. Я подозреваю, что msvc и clang ошибочно проверяют, можете ли вы неявно преобразовать {} в basimpl при проверке ctor копирования, предполагайте эту возможность через конструктор по умолчанию и не проверяйте вовремя, если это удалено. Мне кажется интересным, что если я не удалю его явно, эта ошибка не возникнет. godbolt.org/z/9qTdh36Kq Похоже, он считается присутствующим только после явного удаления, без удаления он вообще не выходит.
Я не уверен в том, что «поскольку конструктор по умолчанию BaseImpl
удален, не существует определяемой пользователем последовательности преобразования из {}
в BaseImpl
». Обычно удаленные объявления видны для разрешения перегрузки и могут быть выбранной перегрузкой, даже если существуют другие неудаленные жизнеспособные функции.
«Совершенно определенно существует определенная пользователем последовательность преобразования из {}
в std::vector<std::string>
». Почему это так? Преобразования применяются к объектам, а {}
не является объектом.
Если бы не было определяемой пользователем последовательности преобразования из {}
в std::vector<std::string>
, никакая функция, принимающая std::vector<std::string>
, не могла бы быть вызвана с {}
в качестве параметра; это также требует неявной последовательности преобразования. Интересен тот факт, что удаленные функции видны для разрешения перегрузки; мне не приходило в голову, что последовательность не будет нарушена удаленной функцией. Однако чем больше я об этом думаю, тем больше мне это кажется. Хм. Я вернусь к этому вечером; до тех пор я отмечу вывод как сомнительный и отошлю людей к комментариям
Clang и MSVC верны, а в gcc есть ошибка.
Определение функции как удаленной — это не то же самое, что не объявлять функцию.
За исключением конструкторов перемещения, функций назначения перемещения и некоторых случаев унаследованных конструкторов (см. [over.match.funcs]/8 ), удаленная функция считается существующей для целей разрешения перегрузки. Ничто другое в разделе [over] не обрабатывает удаленную функцию особым образом. А у нас [over.best.ics]/2, выделение мое:
Последовательности неявного преобразования связаны только с типом, квалификацией cv и категорией значения аргумента, а также с тем, как они преобразуются для соответствия соответствующим свойствам параметра. [Примечание: другие свойства, такие как время жизни, класс хранения, выравнивание, доступность аргумента, является ли аргумент битовым полем и удаляется ли функция, игнорируются. Таким образом, хотя неявная последовательность преобразования может быть определена для данной пары аргумент-параметр, преобразование из аргумента в параметр все же может оказаться некорректным в окончательном анализе. — примечание в конце]
Таким образом, внутри impl() : BaseImpl({}) {}
инициализатор BaseImpl
использует разрешение перегрузки для выбора конструктора BaseImpl
, используемого для инициализации подобъекта базового класса. Кандидатами являются все конструкторы BaseImpl
: предполагаемый BaseImpl(const std::vector<std::string>&)
, удаленный BaseImpl()
, неявно объявленный конструктор копирования BaseImpl(const BaseImpl&)
и неявно объявленный (а не удаленный) конструктор перемещения BaseImpl(BaseImpl&&)
. На данный момент BaseImpl()
нежизнеспособен, так как инициализатор имеет один аргумент. Конструктор векторов является жизнеспособным, поскольку существует конструктор vector(std::initializer_list<std::string>)
, который не является явным и может преобразовывать аргумент {}
в векторный тип. Конструкторы копирования и перемещения также жизнеспособны, поскольку конструктор BaseImpl()
объявлен, не является явным и «может» преобразовать аргумент {}
в тип BaseImpl
. Таким образом, разрешение перегрузки неоднозначно, хотя некоторые неявные последовательности преобразования используют удаленную функцию.
Когда объявление BaseImpl() = delete;
отсутствует, у BaseImpl
просто нет конструктора по умолчанию, поскольку объявление BaseImpl(const std::vector<std::string>&)
предотвращает неявное объявление конструктора по умолчанию. Таким образом, нет неявной последовательности преобразования для {}
в BaseImpl
, а конструкторы копирования и перемещения BaseImpl
не подходят для инициализации BaseImpl({})
. Конструктор векторов — единственная жизнеспособная функция.
Когда вы объявляете BaseImpl(BaseImpl&)
, удаленным или нет, это считается конструктором копирования (несмотря на отсутствие обычного const
), поэтому он предотвращает неявные объявления конструктора копирования и конструктора перемещения. Но этот конструктор копирования не подходит для BaseImpl({})
, поскольку ссылка на неконстантный тип не может привязываться к временному BaseImpl
объекту rvalue, участвующему в использовании BaseImpl()
(см. [over.ics.ref]/3). Таким образом, жизнеспособным является только предполагаемый векторный конструктор.
То есть для выбора перегруженной функции не имеет значения, сформирована последовательность преобразования или нет, имеет значение только то, что она существует? Полезно знать, спасибо.
пожалуйста, включите всю важную информацию в вопрос (сообщение об ошибке компилятора). Кроме того, вместо того, чтобы описывать, что нужно изменить в коде, чтобы получить какую-то другую ошибку или не получить ошибку, всегда лучше опубликовать код с изменениями.