Является ли C ++ 11 Uniform Initialization заменой синтаксиса старого стиля?

172

Я понимаю, что равномерная инициализация C ++ 11 решает некоторую синтаксическую неоднозначность в языке, но во многих презентациях Бьярна Страуструпа (особенно во время выступлений на GoingNative 2012) его примеры в основном используют этот синтаксис сейчас, когда он конструирует объекты.

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

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

void.pointer
источник
Этот вопрос может быть лучше обсужден на Programmers.se. Кажется, склоняется в сторону хорошего субъективного.
Николь Болас
6
@NicolBolas: С другой стороны, ваш отличный ответ может быть очень хорошим кандидатом на тег c ++ - faq. Я не думаю, что у нас было объяснение этому опубликованному ранее.
Матье М.

Ответы:

233

Стиль кодирования в конечном счете субъективен, и весьма маловероятно, что он принесет существенные преимущества в производительности. Но вот что я бы сказал, что вы получаете от либерального использования единой инициализации:

Минимизирует избыточные Typenames

Учтите следующее:

vec3 GetValue()
{
  return vec3(x, y, z);
}

Зачем мне печатать vec3дважды? Есть ли смысл в этом? Компилятор хорошо знает, что возвращает функция. Почему я не могу просто сказать "вызвать конструктор того, что я возвращаю с этими значениями, и вернуть его?" При равномерной инициализации я могу:

vec3 GetValue()
{
  return {x, y, z};
}

Все работает.

Еще лучше для аргументов функции. Учти это:

void DoSomething(const std::string &str);

DoSomething("A string.");

Это работает без необходимости вводить типовое имя, потому что std::stringзнает, как создать себя из const char*неявного. Замечательно. Но что, если эта строка пришла, скажем, RapidXML. Или строка Lua. То есть, допустим, я на самом деле знаю длину строки впереди. std::stringКонструктор , который принимает const char*придется взять длину строки , если я просто пропускать const char*.

Существует перегрузка, которая явно принимает длину. Но использовать его, я должен был бы сделать это: DoSomething(std::string(strValue, strLen)). Почему там есть дополнительное имя типа? Компилятор знает, что это за тип. Так же, как и с auto, мы можем избежать дополнительных названий типов:

DoSomething({strValue, strLen});

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

Конечно, есть аргументы в пользу того, что первая версия ( DoSomething(std::string(strValue, strLen))) более разборчива. То есть очевидно, что происходит и кто что делает. Это правда, до некоторой степени; понимание единого кода, основанного на инициализации, требует рассмотрения прототипа функции. Это та же самая причина, по которой некоторые говорят, что вы никогда не должны передавать параметры по неконстантной ссылке: чтобы вы могли видеть на сайте вызовов, если значение изменяется.

Но то же самое можно сказать и для auto; Знание того, что вы получаете, auto v = GetSomething();требует рассмотрения определения GetSomething. Но это не помешало autoиспользовать его с почти безрассудным отказом, как только вы получите к нему доступ. Лично я думаю, что все будет хорошо, когда ты к этому привыкнешь. Особенно с хорошей IDE.

Никогда не получай самый волнующий разбор

Вот код

class Bar;

void Func()
{
  int foo(Bar());
}

Поп-викторина: что это foo? Если вы ответили «переменная», вы ошибаетесь. На самом деле это прототип функции, которая принимает в качестве параметра функцию, которая возвращает a Bar, а fooвозвращаемое значение функции - int.

Это называется C ++ "Most Vexing Parse", потому что это не имеет абсолютно никакого смысла для человека. Но правила C ++, к сожалению, требуют этого: если это можно интерпретировать как прототип функции, то так и будет . Проблема в том Bar(), это может быть одной из двух вещей. Это может быть тип с именем Bar, что означает, что он создает временный. Или это может быть функция, которая не принимает параметров и возвращает a Bar.

Равномерная инициализация не может быть интерпретирована как прототип функции:

class Bar;

void Func()
{
  int foo{Bar{}};
}

Bar{}всегда создает временный. int foo{...}всегда создает переменную.

Есть много случаев, когда вы хотите использовать, Typename()но просто не можете из-за правил синтаксического анализа в C ++. С Typename{}, нет никакой двусмысленности.

Причины не

Единственная реальная сила, от которой вы отказываетесь, - это сужение. Вы не можете инициализировать меньшее значение большим значением с равномерной инициализацией.

int val{5.2};

Это не скомпилируется. Вы можете сделать это со старомодной инициализацией, но не с равномерной инициализацией.

Это было сделано частично для того, чтобы списки инициализаторов действительно работали. В противном случае было бы много неоднозначных случаев в отношении типов списков инициализатора.

Конечно, некоторые могут утверждать, что такой код не заслуживает компиляции. Я лично согласен; сужение очень опасно и может привести к неприятному поведению. Вероятно, лучше всего уловить эти проблемы на ранней стадии компиляции. По крайней мере, сужение предполагает, что кто-то не слишком задумывается о коде.

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

Есть еще одна причина не:

std::vector<int> v{100};

Что это делает? Он может создать vector<int>сотню созданных по умолчанию предметов. Или это может создать vector<int>с 1 предметом, чья ценность 100. Оба теоретически возможны.

В действительности это делает последнее.

Почему? Списки инициализаторов используют тот же синтаксис, что и при равномерной инициализации. Таким образом, должны быть некоторые правила, чтобы объяснить, что делать в случае двусмысленности. Правило довольно простое: если компилятор может использовать конструктор списка инициализатора со списком, инициализированным фигурной скобкой, то он будет . Так как vector<int>имеет конструктор списка инициализаторов, который принимает initializer_list<int>, и {100} может быть допустимым initializer_list<int>, поэтому он должен быть .

Чтобы получить конструктор размеров, вы должны использовать ()вместо {}.

Обратите внимание, что если бы это было vectorчто-то, что не может быть преобразовано в целое число, этого бы не произошло. Инициализатор initializer_list не подходит для конструктора списка инициализаторов этого vectorтипа, и поэтому компилятор может свободно выбирать из других конструкторов.

Николь Болас
источник
11
+1 прибил это. Я удаляю свой ответ, так как ваш адрес все те же пункты более подробно.
Р. Мартиньо Фернандес
21
Последний пункт, почему я действительно хотел бы std::vector<int> v{100, std::reserve_tag};. Аналогично с std::resize_tag. В настоящее время требуется два шага, чтобы зарезервировать векторное пространство.
Xeo
6
@NicolBolas - Два момента: я думал, что проблема с неприятным синтаксическим анализом была foo (), а не Bar (). Другими словами, если бы вы это сделали int foo(10), разве вы не столкнулись бы с той же проблемой? Во-вторых, другая причина не использовать его, кажется, больше связана с чрезмерным проектированием, но что, если мы создадим все наши объекты с использованием {}, но через день я добавлю конструктор для списков инициализации? Теперь все мои операторы конструкции превращаются в операторы списка инициализатора. Кажется очень хрупким с точки зрения рефакторинга. Есть комментарии по этому поводу?
void.pointer
7
@RobertDailey: «Если бы ты это сделал int foo(10), ты бы не столкнулся с той же проблемой?» № 10 - это целочисленный литерал, а целочисленный литерал никогда не может быть типизированным. Раздражительный синтаксический анализ происходит из-за того, что это Bar()может быть имя типа или временное значение. Вот что создает неоднозначность для компилятора.
Николь Болас
8
unpleasant behavior- есть новый стандартный термин для запоминания:>
сехе
64

Я собираюсь не согласиться с разделом ответа Николя Боласа « Минимизирует избыточные имена» . Поскольку код пишется один раз и читается несколько раз, мы должны стараться минимизировать количество времени, которое требуется для чтения и понимания кода, а не количество времени, которое требуется для написания кода. Попытка просто минимизировать ввод текста - это попытка оптимизировать не то, что нужно.

Смотрите следующий код:

vec3 GetValue()
{
  <lots and lots of code here>
  ...
  return {x, y, z};
}

Кто-то, читающий приведенный выше код в первый раз, вероятно, не сразу поймет оператор return, потому что к тому времени, когда он достигнет этой строки, он забудет о типе возврата. Теперь ему придется прокрутить назад к сигнатуре функции или использовать некоторую функцию IDE, чтобы увидеть тип возвращаемого значения и полностью понять оператор возврата.

И здесь опять же непросто, чтобы кто-то впервые читал код, чтобы понять, что на самом деле создается:

void DoSomething(const std::string &str);
...
const char* strValue = ...;
size_t strLen = ...;

DoSomething({strValue, strLen});

Приведенный выше код сломается, когда кто-то решит, что DoSomething также должен поддерживать какой-то другой тип строки, и добавляет эту перегрузку:

void DoSomething(const CoolStringType& str);

Если у CoolStringType есть конструктор, который принимает const char * и size_t (как это делает std :: string), то вызов DoSomething ({strValue, strLen}) приведет к ошибке неоднозначности.

Мой ответ на реальный вопрос:
нет, универсальная инициализация не должна рассматриваться как замена синтаксиса конструктора старого стиля.

И я рассуждаю так:
если два утверждения не имеют одинакового намерения, они не должны выглядеть одинаково. Есть два вида понятий инициализации объекта:
1) Возьмите все эти предметы и добавьте их в этот объект, который я инициализирую.
2) Построить этот объект, используя эти аргументы, которые я привел в качестве руководства.

Примеры использования понятия № 1:

struct Collection
{
    int first;
    char second;
    double third;
};

Collection c {1, '2', 3.0};
std::array<int, 3> a {{ 1, 2, 3 }};
std::map<int, char> m { {1, '1'}, {2, '2'}, {3, '3'} };

Пример использования понятия № 2:

class Stairs
{
    std::vector<float> stepHeights;

public:
    Stairs(float initHeight, int numSteps, float stepHeight)
    {
        float height = initHeight;

        for (int i = 0; i < numSteps; ++i)
        {
            stepHeights.push_back(height);
            height += stepHeight;
        }
    }
};

Stairs s (2.5, 10, 0.5);

Я думаю, это плохо, что новый стандарт позволяет людям инициализировать лестницу следующим образом:

Stairs s {2, 4, 6};

... потому что это запутывает смысл конструктора. Подобная инициализация выглядит как понятие № 1, но это не так. Он не выливает три разных значения высоты шага в объект s, даже если он выглядит так, как есть. А также, что более важно, если была опубликована реализация библиотеки Stairs, подобная описанной выше, и ее использовали программисты, а затем, если разработчик библиотеки позже добавляет конструктор initializer_list к Stairs, то весь код, который использовал Stairs с Uniform Initialization Синтаксис собирается сломаться.

Я думаю, что сообщество C ++ должно согласиться с общим соглашением о том, как используется Uniform Initialization, то есть единообразно для всех инициализаций, или, как я настоятельно рекомендую, разделить эти два понятия инициализации и тем самым прояснить намерение программиста для читателя код.


ПОСЛЕДУЮЩЕЕ:
Вот еще одна причина, почему вы не должны думать о равномерной инициализации как замене старого синтаксиса, и почему вы не можете использовать скобки для всех инициализаций:

Скажем, ваш предпочтительный синтаксис для создания копии:

T var1;
T var2 (var1);

Теперь вы думаете, что вам следует заменить все инициализации новым синтаксисом скобок, чтобы вы могли (и код будет выглядеть) более согласованным. Но синтаксис с использованием фигурных скобок не работает, если тип T является агрегатом:

T var2 {var1}; // fails if T is std::array for example
TommiT
источник
48
Если у вас есть «<много и много кода здесь>», ваш код будет трудно понять независимо от синтаксиса.
Кевин Клайн
8
Помимо IMO, ваша IDE обязана сообщать вам, какой тип он возвращает (например, через зависание). Конечно, если вы не используете IDE, вы взяли на себя бремя :)
abergmeier
4
@ TommiT Я согласен с некоторыми частями того, что вы говорите. Тем не менее, в том же духе, что и при обсуждении объявления явного типаauto против , я бы поспорил о балансе: унифицированные инициализаторы качают довольно большое время в ситуациях метапрограммирования шаблонов, где тип в любом случае обычно довольно очевиден. Это позволит избежать повторения вашего штрафа, сложного для заклинания, например, для простых шаблонов функции oneline (заставил меня плакать). -> decltype(....)
Сехе
5
« Но синтаксис с использованием фигурных скобок не работает, если тип T является агрегатом: « Обратите внимание, что это зарегистрированный дефект в стандарте, а не преднамеренное, ожидаемое поведение.
Никол Болас
5
«Теперь ему придется вернуться к сигнатуре функции», если вам нужно прокрутить, ваша функция слишком велика.
Майлз Рут
-3

Если ваши конструкторы merely copy their parametersв соответствующих переменных класса, in exactly the same orderв которых они объявлены внутри класса, то использование равномерной инициализации может в конечном итоге оказаться быстрее (но также может быть абсолютно идентичным), чем вызов конструктора.

Очевидно, это не меняет того факта, что вы всегда должны объявлять конструктор.


источник
2
Почему вы говорите, что это может быть быстрее?
Jbcoe
Это неверно Там нет требования , чтобы объявить конструктор: struct X { int i; }; int main() { X x{42}; }. Также неверно, что равномерная инициализация может быть быстрее, чем инициализация значения.
Тим