Я понимаю, что равномерная инициализация C ++ 11 решает некоторую синтаксическую неоднозначность в языке, но во многих презентациях Бьярна Страуструпа (особенно во время выступлений на GoingNative 2012) его примеры в основном используют этот синтаксис сейчас, когда он конструирует объекты.
Рекомендуется ли сейчас использовать единую инициализацию во всех случаях? Каким должен быть общий подход к этой новой функции в том, что касается стиля кодирования и общего использования? Какие есть причины не использовать его?
Обратите внимание, что я думаю, в первую очередь, о конструкции объекта как моем сценарии использования, но если есть другие сценарии, которые необходимо рассмотреть, пожалуйста, дайте мне знать.
Ответы:
Стиль кодирования в конечном счете субъективен, и весьма маловероятно, что он принесет существенные преимущества в производительности. Но вот что я бы сказал, что вы получаете от либерального использования единой инициализации:
Минимизирует избыточные Typenames
Учтите следующее:
Зачем мне печатать
vec3
дважды? Есть ли смысл в этом? Компилятор хорошо знает, что возвращает функция. Почему я не могу просто сказать "вызвать конструктор того, что я возвращаю с этими значениями, и вернуть его?" При равномерной инициализации я могу:Все работает.
Еще лучше для аргументов функции. Учти это:
Это работает без необходимости вводить типовое имя, потому что
std::string
знает, как создать себя изconst char*
неявного. Замечательно. Но что, если эта строка пришла, скажем, RapidXML. Или строка Lua. То есть, допустим, я на самом деле знаю длину строки впереди.std::string
Конструктор , который принимаетconst char*
придется взять длину строки , если я просто пропускатьconst char*
.Существует перегрузка, которая явно принимает длину. Но использовать его, я должен был бы сделать это:
DoSomething(std::string(strValue, strLen))
. Почему там есть дополнительное имя типа? Компилятор знает, что это за тип. Так же, как и сauto
, мы можем избежать дополнительных названий типов:Это просто работает. Нет названий, нет суеты, ничего. Компилятор делает свою работу, код короче, и все счастливы.
Конечно, есть аргументы в пользу того, что первая версия (
DoSomething(std::string(strValue, strLen))
) более разборчива. То есть очевидно, что происходит и кто что делает. Это правда, до некоторой степени; понимание единого кода, основанного на инициализации, требует рассмотрения прототипа функции. Это та же самая причина, по которой некоторые говорят, что вы никогда не должны передавать параметры по неконстантной ссылке: чтобы вы могли видеть на сайте вызовов, если значение изменяется.Но то же самое можно сказать и для
auto
; Знание того, что вы получаете,auto v = GetSomething();
требует рассмотрения определенияGetSomething
. Но это не помешалоauto
использовать его с почти безрассудным отказом, как только вы получите к нему доступ. Лично я думаю, что все будет хорошо, когда ты к этому привыкнешь. Особенно с хорошей IDE.Никогда не получай самый волнующий разбор
Вот код
Поп-викторина: что это
foo
? Если вы ответили «переменная», вы ошибаетесь. На самом деле это прототип функции, которая принимает в качестве параметра функцию, которая возвращает aBar
, аfoo
возвращаемое значение функции - int.Это называется C ++ "Most Vexing Parse", потому что это не имеет абсолютно никакого смысла для человека. Но правила C ++, к сожалению, требуют этого: если это можно интерпретировать как прототип функции, то так и будет . Проблема в том
Bar()
, это может быть одной из двух вещей. Это может быть тип с именемBar
, что означает, что он создает временный. Или это может быть функция, которая не принимает параметров и возвращает aBar
.Равномерная инициализация не может быть интерпретирована как прототип функции:
Bar{}
всегда создает временный.int foo{...}
всегда создает переменную.Есть много случаев, когда вы хотите использовать,
Typename()
но просто не можете из-за правил синтаксического анализа в C ++. СTypename{}
, нет никакой двусмысленности.Причины не
Единственная реальная сила, от которой вы отказываетесь, - это сужение. Вы не можете инициализировать меньшее значение большим значением с равномерной инициализацией.
Это не скомпилируется. Вы можете сделать это со старомодной инициализацией, но не с равномерной инициализацией.
Это было сделано частично для того, чтобы списки инициализаторов действительно работали. В противном случае было бы много неоднозначных случаев в отношении типов списков инициализатора.
Конечно, некоторые могут утверждать, что такой код не заслуживает компиляции. Я лично согласен; сужение очень опасно и может привести к неприятному поведению. Вероятно, лучше всего уловить эти проблемы на ранней стадии компиляции. По крайней мере, сужение предполагает, что кто-то не слишком задумывается о коде.
Обратите внимание, что компиляторы, как правило, предупреждают вас об этом, если ваш уровень предупреждения высок. Так что, на самом деле, все, что делает, это превращает предупреждение в вынужденную ошибку. Некоторые могут сказать, что вы все равно должны это делать;)
Есть еще одна причина не:
Что это делает? Он может создать
vector<int>
сотню созданных по умолчанию предметов. Или это может создатьvector<int>
с 1 предметом, чья ценность100
. Оба теоретически возможны.В действительности это делает последнее.
Почему? Списки инициализаторов используют тот же синтаксис, что и при равномерной инициализации. Таким образом, должны быть некоторые правила, чтобы объяснить, что делать в случае двусмысленности. Правило довольно простое: если компилятор может использовать конструктор списка инициализатора со списком, инициализированным фигурной скобкой, то он будет . Так как
vector<int>
имеет конструктор списка инициализаторов, который принимаетinitializer_list<int>
, и {100} может быть допустимымinitializer_list<int>
, поэтому он должен быть .Чтобы получить конструктор размеров, вы должны использовать
()
вместо{}
.Обратите внимание, что если бы это было
vector
что-то, что не может быть преобразовано в целое число, этого бы не произошло. Инициализатор initializer_list не подходит для конструктора списка инициализаторов этогоvector
типа, и поэтому компилятор может свободно выбирать из других конструкторов.источник
std::vector<int> v{100, std::reserve_tag};
. Аналогично сstd::resize_tag
. В настоящее время требуется два шага, чтобы зарезервировать векторное пространство.int foo(10)
, разве вы не столкнулись бы с той же проблемой? Во-вторых, другая причина не использовать его, кажется, больше связана с чрезмерным проектированием, но что, если мы создадим все наши объекты с использованием{}
, но через день я добавлю конструктор для списков инициализации? Теперь все мои операторы конструкции превращаются в операторы списка инициализатора. Кажется очень хрупким с точки зрения рефакторинга. Есть комментарии по этому поводу?int foo(10)
, ты бы не столкнулся с той же проблемой?» № 10 - это целочисленный литерал, а целочисленный литерал никогда не может быть типизированным. Раздражительный синтаксический анализ происходит из-за того, что этоBar()
может быть имя типа или временное значение. Вот что создает неоднозначность для компилятора.unpleasant behavior
- есть новый стандартный термин для запоминания:>Я собираюсь не согласиться с разделом ответа Николя Боласа « Минимизирует избыточные имена» . Поскольку код пишется один раз и читается несколько раз, мы должны стараться минимизировать количество времени, которое требуется для чтения и понимания кода, а не количество времени, которое требуется для написания кода. Попытка просто минимизировать ввод текста - это попытка оптимизировать не то, что нужно.
Смотрите следующий код:
Кто-то, читающий приведенный выше код в первый раз, вероятно, не сразу поймет оператор return, потому что к тому времени, когда он достигнет этой строки, он забудет о типе возврата. Теперь ему придется прокрутить назад к сигнатуре функции или использовать некоторую функцию IDE, чтобы увидеть тип возвращаемого значения и полностью понять оператор возврата.
И здесь опять же непросто, чтобы кто-то впервые читал код, чтобы понять, что на самом деле создается:
Приведенный выше код сломается, когда кто-то решит, что DoSomething также должен поддерживать какой-то другой тип строки, и добавляет эту перегрузку:
Если у CoolStringType есть конструктор, который принимает const char * и size_t (как это делает std :: string), то вызов DoSomething ({strValue, strLen}) приведет к ошибке неоднозначности.
Мой ответ на реальный вопрос:
нет, универсальная инициализация не должна рассматриваться как замена синтаксиса конструктора старого стиля.
И я рассуждаю так:
если два утверждения не имеют одинакового намерения, они не должны выглядеть одинаково. Есть два вида понятий инициализации объекта:
1) Возьмите все эти предметы и добавьте их в этот объект, который я инициализирую.
2) Построить этот объект, используя эти аргументы, которые я привел в качестве руководства.
Примеры использования понятия № 1:
Пример использования понятия № 2:
Я думаю, это плохо, что новый стандарт позволяет людям инициализировать лестницу следующим образом:
... потому что это запутывает смысл конструктора. Подобная инициализация выглядит как понятие № 1, но это не так. Он не выливает три разных значения высоты шага в объект s, даже если он выглядит так, как есть. А также, что более важно, если была опубликована реализация библиотеки Stairs, подобная описанной выше, и ее использовали программисты, а затем, если разработчик библиотеки позже добавляет конструктор initializer_list к Stairs, то весь код, который использовал Stairs с Uniform Initialization Синтаксис собирается сломаться.
Я думаю, что сообщество C ++ должно согласиться с общим соглашением о том, как используется Uniform Initialization, то есть единообразно для всех инициализаций, или, как я настоятельно рекомендую, разделить эти два понятия инициализации и тем самым прояснить намерение программиста для читателя код.
ПОСЛЕДУЮЩЕЕ:
Вот еще одна причина, почему вы не должны думать о равномерной инициализации как замене старого синтаксиса, и почему вы не можете использовать скобки для всех инициализаций:
Скажем, ваш предпочтительный синтаксис для создания копии:
Теперь вы думаете, что вам следует заменить все инициализации новым синтаксисом скобок, чтобы вы могли (и код будет выглядеть) более согласованным. Но синтаксис с использованием фигурных скобок не работает, если тип T является агрегатом:
источник
auto
против , я бы поспорил о балансе: унифицированные инициализаторы качают довольно большое время в ситуациях метапрограммирования шаблонов, где тип в любом случае обычно довольно очевиден. Это позволит избежать повторения вашего штрафа, сложного для заклинания, например, для простых шаблонов функции oneline (заставил меня плакать).-> decltype(....)
Если ваши конструкторы
merely copy their parameters
в соответствующих переменных класса,in exactly the same order
в которых они объявлены внутри класса, то использование равномерной инициализации может в конечном итоге оказаться быстрее (но также может быть абсолютно идентичным), чем вызов конструктора.Очевидно, это не меняет того факта, что вы всегда должны объявлять конструктор.
источник
struct X { int i; }; int main() { X x{42}; }
. Также неверно, что равномерная инициализация может быть быстрее, чем инициализация значения.