Чи є C ++ 11 Уніфікована ініціалізація заміною синтаксису старого стилю?


172

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

Чи рекомендується зараз використовувати рівномірну ініціалізацію у всіх випадках? Яким повинен бути загальний підхід до цієї нової функції, що стосується стилю кодування та загального використання? Які причини не використовувати його?

Зауважте, що в моєму розумінні я думаю, головним чином, про побудову об'єктів як про моє використання, але якщо є інші сценарії, які слід враховувати, будь ласка, повідомте мене про це.


Це може бути темою, яка краще обговорюється на Programmers.se. Наче схиляється до Доброго Суб'єктивного боку.
Ніколь Болас

6
@NicolBolas: З іншого боку, ваша чудова відповідь може бути дуже хорошим кандидатом на тег c ++ - faq. Я не думаю, що раніше у нас було пояснення щодо цього.
Матьє М.

Відповіді:


233

Стиль кодування в кінцевому підсумку суб'єктивний, і малоймовірно, що від цього вийдуть значні переваги від продуктивності. Але ось що я б сказав, що ви отримуєте від ліберального використання рівномірної ініціалізації:

Мінімізує надлишкові назви

Розглянемо наступне:

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. Або струна Луа. Тобто, скажімо, я фактично знаю довжину струни спереду. std::stringКонструктор , який приймає const char*доведеться взяти довжину рядка , якщо я просто пропускати const char*.

Існує перевантаження, яка явно займає довжину. Але використовувати його, я повинен був би зробити це: DoSomething(std::string(strValue, strLen)). Чому там додаткове ім’я типу? Компілятор знає, що таке тип. Як і у випадку auto, ми можемо уникати додаткових імен:

DoSomething({strValue, strLen});

Це просто працює. Ніяких імен, без суєти, нічого. Компілятор робить свою роботу, код коротший, і всі радіють.

Зрозуміло, що можна зробити аргументи, що перша версія ( DoSomething(std::string(strValue, strLen))) є більш розбірливою. Тобто, очевидно, що відбувається і хто що робить. Це правда, певною мірою; розуміння коду на основі рівномірної ініціалізації вимагає перегляду прототипу функції. Це та сама причина, чому деякі кажуть, що ви ніколи не повинні передавати параметри за допомогою non-const посилання: щоб ви могли бачити на сайті виклику, якщо значення змінюється.

Але те саме можна сказати auto; знати, що ви отримуєте, auto v = GetSomething();вимагає ознайомитися з визначенням GetSomething. Але це не перестало autoвикористовувати з майже необачним відмовою, як тільки ви отримаєте доступ до нього. Особисто я думаю, що це буде добре, коли звикнеш. Особливо з хорошим IDE.

Ніколи не отримуйте самого роздратування

Ось якийсь код.

class Bar;

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

Поп-вікторина: що таке foo? Якщо ви відповіли "змінною", ви помиляєтесь. Це фактично прототип функції, яка приймає за свій параметр функцію, яка повертає a Bar, а fooзначення повернення функції - int.

Це називається "Найпопулярнішим розбором" С ++, оскільки воно абсолютно не має сенсу для людини. Але правила С ++, на жаль, вимагають цього: якщо це можливо інтерпретувати як прототип функції, то воно буде . Проблема в тому 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щось, що не перетворювалося на ціле число, цього не сталося. Ініціалізатор_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- є новий standardese термін пам'ятати:>
sehe

64

Я не погоджуюся з розділом відповіді Ніколя Боласа Мінімізує надлишкові назви . Оскільки код пишеться один раз і читається кілька разів, ми повинні намагатися мінімізувати кількість часу, необхідного для читання та розуміння коду, а не кількість часу, необхідного для написання коду. Намагаючись просто мінімізувати введення тексту, намагається оптимізувати неправильну річ.

Дивіться наступний код:

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

Хтось, хто читає код вище вперше, ймовірно, не зрозуміє негайно твердження про повернення, оскільки до того моменту, коли він досягне цього рядка, він забув про тип повернення. Тепер йому доведеться прокручувати назад до підпису функції або використовувати якусь функцію 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, навіть незважаючи на те, що воно є. А також, що ще важливіше, якщо бібліотечна реалізація схожих сходів була опублікована, а програмісти використовували її, а потім, якщо впроваджувач бібліотеки пізніше додає конструктор ініціалізатора_list до сходів, то весь код, який використовує сходи з рівномірною ініціалізацією Синтаксис зірветься.

Я думаю, що спільнота C ++ повинна погодитись на загальну конвенцію про те, як використовується Уніфікована ініціалізація, тобто рівномірно щодо всіх ініціалізацій, або, як я настійно пропоную, розділити ці два поняття ініціалізації та таким чином уточнити намір програміста читачеві код.


ПІСЛЯ:
Ось ще одна причина, чому ви не повинні вважати Уніфіковану ініціалізацію заміною старого синтаксису, і чому ви не можете використовувати позначення дужок для всіх ініціалізацій:

Скажімо, ваш кращий синтаксис для створення копії:

T var1;
T var2 (var1);

Тепер ви думаєте, що слід замінити всі ініціалізації на новий синтаксис дужки, щоб ви могли бути (і код буде виглядати) більш послідовним. Але синтаксис, що використовує дужки, не працює, якщо тип T є сукупним:

T var2 {var1}; // fails if T is std::array for example

48
Якщо у вас "<lot and lot code"> ваш код буде важко зрозуміти незалежно від синтаксису.
кевін клайн

8
Крім IMO, ваш IDE є обов'язком повідомляти вам, який тип він повертає (наприклад, за допомогою наведення курсора). Звичайно, якщо ви не використовуєте IDE, ви взяли на себе тягар :)
abergmeier

4
@TommiT Я погоджуюся з деякими частинами того, що ви говорите. Однак, в тому ж дусі, що і autoпроти явної декларації типу , я б заперечував за рівновагу: уніфіковані ініціалізатори роблять досить великий час у ситуаціях метапрограмування шаблонів, де тип, як правило, досить очевидний. Це дозволить уникнути повторення вашого прокляття, складного -> decltype(....)для заклинання, наприклад, для простих однолінійних шаблонів функцій (змусив мене плакати).
sehe

5
" Але синтаксис, що використовує дужки, не працює, якщо тип T є сукупним: " Зауважте, що це повідомляється про дефект стандартної, а не навмисної очікуваної поведінки.
Нікол Болас

5
"Тепер йому доведеться прокрутити назад до підпису функції", якщо вам доведеться прокрутити, ваша функція занадто велика.
Майлз Рут

-3

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

Очевидно, це не змінює того факту, що ви завжди повинні оголошувати конструктор.


2
Чому ти кажеш, що це може бути швидше?
jbcoe

Це неправильно. Там немає вимоги , щоб оголосити конструктор: struct X { int i; }; int main() { X x{42}; }. Неправильно також, що рівномірна ініціалізація може бути швидшою, ніж ініціалізація значення.
Тім
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.