Что может пойти не так, если инициализация копирования списка позволила явные конструкторы? - PullRequest
38 голосов
/ 06 февраля 2012

В стандарте C ++, §13.3.1.7 [over.match.list], указано следующее:

В copy-list-initialization, если выбран конструктор 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

Так как запрет явно указан, у меня такое ощущение, что я что-то здесь упускаю. Насколько я вижу, явные конструкторы выбора инициализации списка не кажутся мне проблемой: используя синтаксис инициализации списка, программист уже выражает желание сделать какое-то «преобразование».

Что может пойти не так? Чего мне не хватает?

Ответы [ 4 ]

25 голосов
/ 06 февраля 2012

Концептуально инициализация copy-list-initialization - это преобразование составного значения в тип назначения.В документе, который предлагал формулировку и объяснял обоснование, термин «копия» в «инициализации списка копий» уже считался неудачным, поскольку он на самом деле не отражает фактического обоснования этого.Но это сохранено для совместимости с существующей формулировкой.{10, 20} значение пары / кортежа не должно быть в состоянии скопировать инициализировать 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}); }

Это также неправильно сформировано из-за неоднозначности.Некоторые люди (включая меня) до выхода C ++ 11 думали, что это неудачно, но не придумали статью, предлагающую изменение в этом отношении (насколько мне известно).

2 голосов
/ 06 февраля 2012

Это утверждение:

В случае инициализации списка копирования, если выбран конструктор explicit, инициализация некорректна.

означает много вещей,Среди них это означает, что он должен смотреть на явные конструкторы.В конце концов, он не может выбрать явный конструктор, если не может посмотреть на него.Когда он ищет кандидатов для преобразования в фигурный список, он должен выбирать из всех кандидатов.Даже те, которые позже будут признаны недопустимыми.

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

2 голосов
/ 06 февраля 2012

Как я понимаю, само назначение ключевого слова явный запрещает неявное приведение с помощью этого конструктора.

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

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
1 голос
/ 06 февраля 2012

Разве это не потому, что 'явное' есть, чтобы остановить неявное приведение, и вы просите его выполнить неявное приведение?

Вы бы задали вопрос, если указали структуру с конструктором с одним аргументом?

...