Почему C ++ 11 не поддерживает списки назначенных инициализаторов как C99? [закрыто]

121

Рассматривать:

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

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

Приведенный выше код допустим для C99, но не для C ++ 11.

Что это было обоснование стандартного комитета для исключения поддержки такой удобной функции?

xmllmx
источник
10
Очевидно, для комитета по дизайну не имело смысла включать его, или он просто не поднимался на собраниях. Стоит отметить, что назначенные инициализаторы C99 не входят ни в одну из версий спецификации C ++. Конструкторы кажутся предпочтительной конструкцией инициализации, и не зря: они гарантируют последовательную инициализацию объекта, если вы напишете их правильно.
Роберт Харви
19
Ваше рассуждение является обратным: у языка не обязательно должно быть обоснование отсутствия функции, ему нужно обоснование для наличия одной, причем сильной. В нынешнем виде C ++ достаточно раздут.
Matthieu M.
42
Хорошая причина (которую нельзя решить с помощью конструкторов, кроме как написанием ошеломляющих оболочек) заключается в том, что независимо от того, используете вы C ++ или нет, большинство реальных API-интерфейсов - это C, а не C ++, и некоторые из них заставляют вас предоставлять структуру, в которой вы хотите одно или два поля - и не обязательно первое - но остальные должны быть инициализированы нулями. Win32 API OVERLAPPEDявляется таким примером. Возможность писать ={.Offset=12345};сделает код более понятным (и, вероятно, менее подверженным ошибкам). Сокеты BSD - аналогичный пример.
Дэймон
14
Неверный код mainC99. Следует читать struct Person p = { .age = 18 };
chqrlie
14
FYI C ++ 20 будет поддерживать назначенные инициализаторы
Эндрю Томазос

Ответы:

34

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

С другой стороны, функция назначенных инициализаторов больше предназначена для раскрытия и упрощения доступа к членам непосредственно в клиентском коде. Это приводит к таким вещам, как наличие человека 18 (лет?), Но с нулевым ростом и весом.


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

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

bames53
источник
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 фактически поддерживают назначенные инициализаторы, и было множество предложений по добавлению поддержки длястандарт, например: 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:

В списке инициализаторов списка инициализаторов в фигурных скобках предложения инициализатора, включая те, которые являются результатом расширения пакета (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 уже имеет эту проблему, но она находится в определении члена класса, поэтому сильная связь не вызывает удивления. И назначенные инициализаторы не могут ссылаться на другие члены, как это могут сделать инициализаторы-члены ctor.
Ben Voigt
2
@MattMcNabb: Нет, это не более экстремально. Но можно ожидать, что разработчик, реализующий конструктор класса, знает порядок объявления членов. Тогда как потребителем класса может быть совершенно другой программист. Поскольку все дело в том, чтобы разрешить инициализацию без необходимости искать порядок членов, это кажется фатальной ошибкой в ​​предложении. Поскольку назначенные инициализаторы не могут ссылаться на создаваемый объект, первое впечатление заключается в том, что выражения инициализации могут быть оценены сначала в порядке обозначения, а затем инициализация члена в порядке объявления. Но ...
Бен Фойгт
2
@JonathanMee: Ну, другой вопрос ответил, что ... Агрегатные инициализаторы C99 неупорядочены, поэтому нет никаких ожиданий, что назначенные инициализаторы будут упорядочены. Списки инициализации в фигурных скобках С ++ упорядочены, и предложение для назначенных инициализаторов использует потенциально неожиданный порядок (вы не можете быть согласованными как с лексическим порядком, используемым для всех списков инициализации в фигурных скобках, так и с порядком членов, используемым для инициализатора ctor -lists)
Бен Войт
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 $;
}()));
keebus
источник
Аккуратно, создает лямбду с переменной с именем $типа T, и вы назначаете ее члены непосредственно перед ее возвратом. Острота. Интересно, есть ли проблемы с производительностью.
TankorSmash 05
1
В оптимизированной сборке вы не видите следов лямбды или ее вызова. Все это встроено.
keebus
1
Мне очень нравится этот ответ.
Сеф Рид
6
Ого. Даже не знала, что $ действительное имя.
Крис Уоттс
Он поддерживался устаревшими компиляторами C, и поддержка осталась для обратной совместимости.
keebus
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самом деле это не постоянная времени компиляции.

wcy
источник
8
А теперь попробуйте constexpr point(int _x,int _y):x(_x),y(_y){}. Оптимизатор clang ++, похоже, также устраняет сравнение в вашем коде. Итак, это просто проблема QoI.
dyp
Я также ожидаю, что весь объект a_point будет оптимизирован, если у него будет внутренняя связь. т.е. поместите его в анонимное пространство имен и посмотрите, что произойдет. goo.gl/wNL0HC
Arvid
@dyp: Даже простое определение конструктора возможно только в том случае, если тип находится под вашим контролем. Вы не можете сделать это, например, для struct addrinfoили struct sockaddr_in, поэтому вам остаются присваивания отдельно от объявлений.
musiphil
2
@musiphil По крайней мере, в C ++ 14 эти структуры в стиле C можно правильно настроить в функции constexpr как локальные переменные с помощью присваивания, а затем вернуть их из этой функции. Кроме того, я хотел показать не альтернативную реализацию конструктора на C ++, которая допускает оптимизацию, а продемонстрировать, что компилятор может выполнить эту оптимизацию, если форма инициализации отличается. Если компилятор «достаточно хорош» (т.е. поддерживает эту форму оптимизации), то не имеет значения, используете ли вы ctor, назначенные инициализаторы или что-то еще.
dyp