Чому C ++ 11 не підтримує призначені списки ініціалізаторів як C99? [зачинено]


121

Поміркуйте:

struct Person
{
    int height;
    int weight;
    int age;
};

int main()
{
    Person p { .age = 18 };
}

Код, наведений вище, є законним у C99, але не є законним у C ++ 11

Що було стандартне обґрунтування комітету для виключення підтримки такої зручної функції?


10
Мабуть, проектному комітету не було сенсу включати його, або він просто не прийшов на засідання. Варто зазначити, що призначені для C99 ініціалізатори не містяться ні в одній із версій специфікації C ++. Конструктори здаються кращою конструкцією ініціалізації, і з поважної причини: вони гарантують послідовну ініціалізацію об'єкта, якщо ви правильно їх записуєте.
Роберт Харві

19
Ваші міркування відсталі, мова не повинна обґрунтовувати відсутність ознаки, вона потребує обґрунтування наявності одного і сильного при цьому. С ++ досить роздутий, як і досі.
Маттьє М.

42
Хороша причина (яку неможливо вирішити за допомогою конструкторів, за винятком написання загортаючих обгортків) полягає в тому, що ви використовуєте чи не використовуєте C ++, більшість справжніх API - це C, а не C ++, і не мало з них змушує вас поставити структуру, в якій ви хочете встановити одне або два поля - і не обов’язково перше -, але решта потрібно ініціалізувати нуль. API Win32 OVERLAPPED- такий приклад. Можливість писати ={.Offset=12345};зробить код набагато зрозумілішим (і, ймовірно, менш схильним до помилок). BSD-розетки - подібний приклад.
Деймон

14
Код у mainзаконі C99 не є законним. Слід прочитати struct Person p = { .age = 18 };
chqrlie

14
FYI C ++ 20 підтримає призначені ініціалізатори
Ендрю Томазос

Відповіді:


34

C ++ має конструктори. Якщо є сенс ініціалізувати лише одного члена, то це можна виразити в програмі, застосувавши відповідний конструктор. Цей вид абстракції C ++ сприяє.

З іншого боку, функція призначеного ініціалізатора стосується більше відкриття та полегшення доступу членів безпосередньо до клієнтського коду. Це призводить до таких речей, як наявність людини у віці 18 (років?), Але з ростом і вагою до нуля.


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

C ++ більше зацікавлений у тому, щоб надати гнучкість на стороні дизайнера типу, тому дизайнери можуть полегшити правильне використання типу та складно використовувати неправильно. Покладання дизайнера на контроль того, як можна ініціалізувати тип, є частиною цього: конструктор визначає конструктори, ініціалізатори в класі тощо.


12
Будь ласка, покажіть посилання на те, що, на вашу думку, є причиною того, що C ++ не має ініціалізаторів. Я не пам'ятаю, як я коли-небудь бачив пропозицію.
Йоханнес Шауб - ліб

20
Чи не сама причина не надавати конструктор для того, Personщоб його автор хотів забезпечити якомога більшу гнучкість для користувачів для встановлення та ініціалізації членів? Користувач також може вже писати Person p = { 0, 0, 18 };(і з поважних причин).
Йоханнес Шауб - ліб

7
Щось подібне нещодавно було прийнято у специфікацію C ++ 14 open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3605.html .
Йоханнес Шауб - ліб

4
@ JohannesSchaub-litb Я не говорю про суто механічну, безпосередню причину (тобто вона не була запропонована комітету). Я описую те, що я вважаю домінуючим фактором. - Personмає дуже дизайн C, тому функції C можуть мати сенс. Однак C ++, ймовірно, забезпечує кращу конструкцію, що також унеможливлює призначення призначених ініціалізаторів. - На мій погляд, зняття обмеження на класові ініціалізатори для агрегатів значно більше відповідає етосу C ++, ніж призначені ініціалізатори.
bames53

4
Заміну C ++ для цього можна назвати аргументами функції. Але на сьогодні аргументи назви офіційно не існують. Див. N4172 Названі аргументи для пропозиції цього. Це зробить код менш схильним до помилок і простішим для читання.
Девід Бейрд

89

15 липня '17 P0329R4 було прийнято до складустандарт: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Це забезпечує обмежену підтримкупризначені ініціалізатори. Це обмеження описано у розділі C.1.7 [diff.decl] .4, якщо:

struct A { int x, y; };
struct B { struct A a; };

Наступні призначені ініціалізації, які діють на C, обмежені в C ++:

  • struct A a = { .y = 1, .x = 2 } недійсний у C ++, тому що позначки повинні відображатися в порядку декларування членів даних
  • int arr[3] = { [1] = 5 } недійсний у C ++, оскільки ініціалізація, визначена масивом, не підтримується
  • struct B b = {.a.x = 0} недійсний у C ++, тому що конструктори не можуть бути вкладені
  • struct A c = {.x = 1, 2} недійсний у C ++, тому що або всі, або жоден із членів даних повинен бути ініціалізований позначеннями

Для і раніше Boost насправді має підтримку призначених Intializer, і було багато пропозицій щодо підтримки підтримкистандарт, наприклад: n4172 та пропозиція Дарила Уокера про додавання позначення до ініціалізаторів . Пропозиції цитують виконанняПозначені ініціалізатори у Visual C ++, gcc та Clang, які заявляють:

Ми вважаємо, що зміни можна буде відносно просто здійснити

Але стандартний комітет неодноразово відхиляє такі пропозиції , заявляючи:

EWG знайшла різні проблеми із запропонованим підходом, і не вважає, що це можливо намагатися вирішити проблему, оскільки це було перепробовано багато разів і кожен раз, коли воно не вдалося

Коментарі Бена Войта допомогли мені побачити непереборні проблеми з таким підходом; дано:

struct X {
    int c;
    char a;
    float b;
};

У якому порядку будуть закликатися ці функції : struct X foo = {.a = (char)f(), .b = g(), .c = h()}? Дивно, але в:

Порядок оцінки підекспресій у будь-якому ініціалізаторі невизначено послідовно [ 1 ]

( Здається, Visual C ++, gcc та Clang мають згоду на поведінку, оскільки всі вони здійснюватимуть дзвінки в цьому порядку :)

  1. h()
  2. f()
  3. g()

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

дійсно має жорсткі вимоги до ініціалізатор-лист 11.6.4 [dcl.init.list] 4:

У списку ініціалізатора списку, встановленого braced-init, засади ініціалізатора, включаючи будь-які, що є результатом розширення пакету (17.5.3), оцінюються в тому порядку, в якому вони з'являються. Тобто, кожне обчислення значення та побічний ефект, пов'язаний із заданим ініціалізаційним пунктом, секвенується перед кожним обчисленням значення та побічним ефектом, пов’язаним із будь-яким ініціалізаторним застереженням, яке слідує за ним у списку відокремлених комами списку ініціалізатора.

Так підтримка вимагала б, щоб це було виконано в порядку:

  1. f()
  2. g()
  3. h()

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


3
Звичайно, у цьому коді: struct X { int c; char a; float b; }; X x = { .a = f(), .b = g(), .c = h() };виклик h()виконувати перед будь-яким f()або g(). Якщо визначення struct Xнемає поблизу, це буде дуже дивно. Пам’ятайте, що вирази ініціалізатора не повинні бути побічними ефектами.
Бен Войгт

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

2
@MattMcNabb: Ні, це не більш екстремально. Але очікується, що розробник, який реалізує конструктор класу, дізнається порядок декларування членів. Тоді як споживач класу може бути цілком іншим програмістом. Оскільки вся справа в тому, щоб дозволити ініціалізацію без необхідності шукати порядок членів, це здається фатальним недоліком у пропозиції. Оскільки призначені ініціалізатори не можуть посилатися на об'єкт, який будується, перше враження полягає в тому, що вирази ініціалізації можна оцінити спочатку в порядку призначення, потім ініціалізації члена в порядку декларування. Але ...
Бен Войгт

2
@JonathanMee: Ну, інше питання відповіло, що ... ініціалізатори агрегатів C99 не упорядковані, тому очікується, що призначені ініціалізатори не будуть замовлені. Впорядковані списки C ++ з впорядкованими ініціалами впорядковані, і пропозиція щодо призначених ініціалізаторів використовує потенційно дивовижний порядок (ви не можете узгоджуватись як з лексичним порядком, який використовується для всіх списків з braced-init, так і порядком членів, що використовується для ctor-ініціалізатора -листи)
Ben Voigt

3
Джонатан: "Підтримка c ++ вимагала б, щоб це було виконано в порядку [...] Порушення сумісності з попередніми реалізаціями c99". Я не розумію цього, вибачте. 1. Якщо замовлення невизначено у C99, очевидно, будь-яке фактичне замовлення повинно бути нормальним, включаючи будь-який довільний вибір C ++. б) Не підтримує дес. ініціалізатори начебто вже порушують сумісність C99 ще більше ...
Sz.

34

Трохи хакерства, тож просто ділитися для розваги.

#define with(T, ...)\
    ([&]{ T ${}; __VA_ARGS__; return $; }())

І використовувати його так:

MyFunction(with(Params,
    $.Name = "Foo Bar",
    $.Age  = 18
));

який розширюється до:

MyFunction(([&] {
 Params ${};
 $.Name = "Foo Bar", $.Age = 18;
 return $;
}()));

Акуратний, створює лямбда зі змінною, названою $типом T, і ви призначаєте її членів безпосередньо перед поверненням. Ніфт. Цікаво, чи є з цим якісь проблеми.
TankorSmash

1
В оптимізованій збірці ви не бачите жодних слідів лямбда та її виклику. Це все накреслено.
кебус

1
Я абсолютно люблю цю відповідь.
Seph Reed

6
Вуа. Навіть не знав, що $ - дійсне ім'я.
Кріс Уоттс

Його підтримували застарілі компілятори C, а підтримка залишалася для зворотної сумісності.
киебус

22

Зазначений ініціалізатор наразі включений до складу роботи C ++ 20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf, щоб ми їх нарешті побачили!


3
Але зауважте, що вони обмежені: У C ++ позначена підтримка ініціалізації обмежена порівняно з відповідною функціональністю у C. У C ++ позначені для нестатичних членів даних повинні бути вказані в порядку декларування, позначення елементів масиву та вкладені позначення не є підтримувані та призначені та не призначені ініціалізатори не можна змішувати в одному списку ініціалізаторів. Це означає, що, зокрема, ви все одно не зможете легко скласти таблицю пошуку із зануреними ключами .
Руслан

@Ruslan: Цікаво, чому C ++ їх так обмежив? Я розумію, що може виникнути плутанина у тому, чи відповідає порядок, у якому значення елементів оцінюються та / або записуються в структуру, відповідає порядку, в якому елементи вказані у списку ініталізації, або порядку, у якому члени фігурують у структурі, але рішення , що було б просто сказати , що вирази ініціалізації виконується в довільній послідовності, а час життя об'єкта не починається до ініціалізації не буде завершено процес ( &оператор поверне адресу, об'єкт буде мати в протягом свого життя).
supercat

5

Дві основні особливості C99, у яких C ++ 11 відсутнє, згадується "призначені ініціалізатори та C ++".

Я думаю, що "призначений ініціалізатор" пов'язаний з потенційною оптимізацією. Тут я використовую “gcc / g ++” 5.1 як приклад.

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>    
struct point {
    int x;
    int y;
};
const struct point a_point = {.x = 0, .y = 0};
int foo() {
    if(a_point.x == 0){
        printf("x == 0");
        return 0;
    }else{
        printf("x == 1");
        return 1;
    }
}
int main(int argc, char *argv[])
{
    return foo();
}

Ми знали, що час компіляції a_point.xдорівнює нулю, тому можна було очікувати, що fooце оптимізовано в єдиний printf.

$ gcc -O3 a.c
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function foo:
   0x00000000004004f0 <+0>: sub    $0x8,%rsp
   0x00000000004004f4 <+4>: mov    $0x4005bc,%edi
   0x00000000004004f9 <+9>: xor    %eax,%eax
   0x00000000004004fb <+11>:    callq  0x4003a0 <printf@plt>
   0x0000000000400500 <+16>:    xor    %eax,%eax
   0x0000000000400502 <+18>:    add    $0x8,%rsp
   0x0000000000400506 <+22>:    retq   
End of assembler dump.
(gdb) x /s 0x4005bc
0x4005bc:   "x == 0"

fooоптимізовано x == 0лише для друку .

Для версії C ++,

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
struct point {
    point(int _x,int _y):x(_x),y(_y){}
    int x;
    int y;
};
const struct point a_point(0,0);
int foo() {
    if(a_point.x == 0){
        printf("x == 0");
        return 0;
    }else{
        printf("x == 1");
        return 1;
    }
}
int main(int argc, char *argv[])
{
    return foo();
}

І це вихід оптимізованого коду збирання.

g++ -O3 a.cc
$ gdb a.out
(gdb) disassemble foo
Dump of assembler code for function _Z3foov:
0x00000000004005c0 <+0>:    push   %rbx
0x00000000004005c1 <+1>:    mov    0x200489(%rip),%ebx        # 0x600a50 <_ZL7a_point>
0x00000000004005c7 <+7>:    test   %ebx,%ebx
0x00000000004005c9 <+9>:    je     0x4005e0 <_Z3foov+32>
0x00000000004005cb <+11>:   mov    $0x1,%ebx
0x00000000004005d0 <+16>:   mov    $0x4006a3,%edi
0x00000000004005d5 <+21>:   xor    %eax,%eax
0x00000000004005d7 <+23>:   callq  0x400460 <printf@plt>
0x00000000004005dc <+28>:   mov    %ebx,%eax
0x00000000004005de <+30>:   pop    %rbx
0x00000000004005df <+31>:   retq   
0x00000000004005e0 <+32>:   mov    $0x40069c,%edi
0x00000000004005e5 <+37>:   xor    %eax,%eax
0x00000000004005e7 <+39>:   callq  0x400460 <printf@plt>
0x00000000004005ec <+44>:   mov    %ebx,%eax
0x00000000004005ee <+46>:   pop    %rbx
0x00000000004005ef <+47>:   retq   

Ми можемо бачити, що a_pointце насправді не постійне значення часу компіляції.


8
Тепер спробуйте constexpr point(int _x,int _y):x(_x),y(_y){}. Оптимізатор clang ++, здається, також усуває порівняння у вашому коді. Отже, це лише питання QI.
dyp

Я також очікував би оптимізувати весь об'єкт a_point, якби він мав внутрішній зв'язок. тобто помістіть його в анонімний простір імен і подивіться, що відбувається. goo.gl/wNL0HC
Арвід

@dyp: Навіть просто визначити конструктор можливо, лише якщо тип знаходиться під вашим контролем. Ви не можете цього зробити, наприклад, для struct addrinfoабо struct sockaddr_in, тому вам залишаються завдання, окремі від декларацій.
musiphil

2
@musiphil Принаймні в C ++ 14, ці структури в стилі C можна правильно встановити у функції constexpr як локальних змінних за допомогою призначення, а потім повернути з цієї функції. Крім того, моя думка полягала не в тому, щоб показати альтернативну реалізацію конструктора в C ++, що дозволяє оптимізувати, але показати, що компілятор може виконати цю оптимізацію, якщо форма ініціалізації відрізняється. Якщо компілятор "досить хороший" (тобто підтримує цю форму оптимізації), то не має значення, чи використовуєте ви ctor або призначені ініціалізатори чи щось інше.
dyp
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.