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

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

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

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

150 голосов | спросил void.pointer 6 FebruaryEurope/MoscowbMon, 06 Feb 2012 06:22:43 +0400000000amMon, 06 Feb 2012 06:22:43 +040012 2012, 06:22:43

3 ответа


212

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

Сведение к минимуму избыточных имен файлов

Рассмотрим следующее:

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());
}

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

Это называется C ++ «Most Vexing Parse», потому что это абсолютно не имеет смысла для человека. Но правила C ++, к сожалению, требуют этого: если его можно интерпретировать как прототип функции, то он будет be. Проблема заключается в Bar(); это может быть одной из двух вещей. Это может быть тип с именем Bar, что означает, что он создает временную. Или это может быть функция, которая не принимает никаких параметров и возвращает 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 того, который не был конвертирован в целое число, это не произойдет. Инициализатор-лист не соответствовал бы конструктору списка инициализаторов этого типа vector, и поэтому компилятор мог бы свободно выбирать из других конструкторов.

ответил Nicol Bolas 6 FebruaryEurope/MoscowbMon, 06 Feb 2012 07:06:11 +0400000000amMon, 06 Feb 2012 07:06:11 +040012 2012, 07:06:11
56

Я собираюсь не согласиться с разделом ответа Никололя Боласа Сведение к минимуму избыточных имен файлов . Поскольку код записывается один раз и читается несколько раз, мы должны попытаться свести к минимуму время, затрачиваемое на читать и понимать код, а не количество времени, которое требуется на запись . > код. Пытаясь просто свести к минимуму ввод текста, вы пытаетесь оптимизировать неправильную вещь.

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

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

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

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

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}) приведет к ошибке двусмысленности.

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

И мои рассуждения таковы:
Если два утверждения не имеют такого же намерения, они не должны выглядеть одинаково. Существует два типа понятий инициализации объекта:
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 ++ должно согласиться с общим соглашением о том, как используется унифицированная инициализация, т.е. равномерно во всех инициализациях, или, как я настоятельно рекомендую, разделяя эти два понятия инициализации и тем самым уточняя намерение программиста читатель кода.


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

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

T var1;
T var2 (var1);

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

T var2 {var1}; // fails if T is std::array for example
ответил TommiT 18 TueEurope/Moscow2012-12-18T08:52:42+04:00Europe/Moscow12bEurope/MoscowTue, 18 Dec 2012 08:52:42 +0400 2012, 08:52:42
-3

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

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

ответил 6 FebruaryEurope/MoscowbMon, 06 Feb 2012 06:29:57 +0400000000amMon, 06 Feb 2012 06:29:57 +040012 2012, 06:29:57

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132