Что может пойти не так, если инициализация списка экземпляров разрешена явным конструкторам?
В стандарте С++, §13.3.1.7 [over.match.list], указано следующее:
В инициализации списка копий, если выбран конструктор explicit
, инициализация плохо сформирована.
Вот почему мы не можем сделать, например, что-то вроде этого:
struct foo {
// explicit because it can be called with one argument
explicit foo(std::string s, int x = 0);
private:
// ...
};
void f(foo x);
f({ "answer", 42 });
(Обратите внимание, что здесь происходит не преобразование, и это было бы не так, даже если конструктор был "неявным". Это инициализация объекта foo
, используя его конструктор напрямую. Помимо std::string
здесь нет преобразования.)
Это кажется мне совершенно прекрасным. Нет никакого способа, чтобы неявное преобразование укусит меня.
Если { "answer", 42 }
может инициализировать что-то еще, компилятор не выдаст меня и не сделает ничего:
struct bar {
// explicit because it can be called with one argument
explicit bar(std::string s, int x = 0);
private:
// ...
};
void f(foo x);
void f(bar x);
f({ "answer", 42 }); // error: ambiguous call
Нет проблем: вызов неоднозначен, код не будет компилироваться, и я должен будет явно явно перегрузить.
f(bar { "answer", 42 }); // ok
Поскольку запрет явно указывается, у меня такое ощущение, что я что-то упустил. Насколько я могу судить, инициализация списка, создающая явные конструкторы, для меня не кажется проблемой: с помощью синтаксиса инициализации списка программист уже выражает желание сделать какое-то "преобразование".
Что может пойти не так? Что мне не хватает?
Ответы
Ответ 1
Концептуально инициализация списка копий - это преобразование составного значения в тип назначения. В документе, который предложил формулировку и объясненное обоснование, уже считалось неудачным термин "копия" в "инициализации списка копий", поскольку на самом деле он не передает фактическое обоснование. Но он поддерживается для совместимости с существующими формулировками. Значение {10, 20}
pair/tuple не должно копировать инициализацию String(int size, int reserve)
, потому что строка не является парой.
Явные конструкторы считаются, но запрещены к использованию. Это имеет смысл в следующих случаях:
struct String {
explicit String(int size);
String(char const *value);
};
String s = { 0 };
0
не передает значение строки. Таким образом, это приводит к ошибке, поскольку рассматриваются оба конструктора, но выбирается конструктор explicit
, вместо 0
рассматривается как константа нулевого указателя.
К сожалению, это также происходит при разрешении перегрузки по функциям
void print(String s);
void print(std::vector<int> numbers);
int main() { print({10}); }
Это плохо сформировано из-за двусмысленности. Некоторые люди (включая меня) до того, как С++ 11 были выпущены, подумали, что это неудачно, но не придумали документ, предлагающий изменение относительно этого (насколько мне известно).
Ответ 2
Как я понимаю, сама цель ключевого слова Явная отрицает неявный листинг с помощью этого конструктора.
Итак, вы спрашиваете, почему явный конструктор нельзя использовать для неявного литья? Очевидно, потому что автор этого конструктора явно отрицал это, используя с ним ключевое слово явное. В цитате из опубликованного вами стандарта указано, что ключевое слово явное применяется также к спискам инициализаторов (а не только к простым значениям некоторого типа).
ADD:
Чтобы сказать более правильно: цель ключевого слова Явная, используемая с каким-либо конструктором, делает абсолютно ясным, что этот конструктор используется в некотором месте (т.е. принуждение всего кода к вызову этого конструктора явно).
И выражение IMO как f({a,b})
, когда f
- это имя функции, не имеет ничего общего с явным вызовом конструктора. Абсолютно непонятно (и зависит от контекста), какой конструктор (и какой тип) используется здесь, например. это зависит от присутствия функций.
С другой стороны, что-то вроде f(SomeType(a,b))
совершенно другое: совершенно очевидно, что мы используем конструктор типа SomeType
, который принимает два аргумента a,b
и что мы используем перегрузку функции f
, которая будет лучше всего принять единственный аргумент типа SomeType
.
Итак, некоторые конструкторы в порядке для неявного использования типа f({a,b})
, а другие требуют, чтобы факт их использования был абсолютно ясен читателю, поэтому мы объявляем их явным.
ADD2:
Моя точка зрения: иногда совершенно разумно объявлять конструкторы явными, даже если ничего не получится. ИМО, является ли конструктор явным, больше зависит от его логики, чем от каких-либо предостережений.
например.
double x = 2; // looks absolutely natural
std::complex<double> x1 = 3; // also looks absolutely natural
std::complex<double> x2 = { 5, 1 }; // also looks absolutely natural
Но
std::vector< std::set<std::string> > seq1 = 7; // looks like nonsense
std::string str = some_allocator; // also looks stupid
Ответ 3
Это утверждение:
В инициализации списка копий, если выбран конструктор explicit
, инициализация плохо сформирована.
означает много вещей. Среди них это означает, что он должен смотреть на явные конструкторы. В конце концов, он не может выбрать явный конструктор, если он не может смотреть на него. Когда он ищет кандидатов для преобразования списка в список, он должен выбрать всех кандидатов. Даже те, которые позже будут признаны незаконными.
Если разрешение перегрузки приводит к одинаковой жизнеспособности нескольких функций, это приводит к неоднозначному вызову, требующему вмешательства пользователя вручную.
Ответ 4
Разве это не потому, что "явный" существует, чтобы остановить неявное кастинг, и вы просите его сделать неявный листинг?
Не могли бы вы задать вопрос, указали ли вы структуру с помощью одного конструктора аргументов?