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

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

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

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

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

Каково было основание комитета по стандарту C++ 11 для исключения поддержки такой удобной функции?

5 ответов

Решение

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

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


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

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

15 июля 1717 P0329R4 был принят в стандарт C++20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Это приносит ограниченную поддержку назначенным инициализаторам c99. Это ограничение описывается следующим образом в 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++, потому что либо все, либо ни один из членов данных не должен быть инициализирован указателями

Для C++17 и более ранних версий Boost фактически имеет поддержку назначенных инициализаторов, и было множество предложений добавить поддержку стандарта C++, например: n4172 и предложение Дэрил Уокер добавить обозначение в инициализаторы. В предложениях приводится реализация назначенных инициализаторов c99 в Visual C++, gcc и Clang, утверждающих:

Мы считаем, что изменения будут относительно простыми для реализации

Но комитет по стандартизации неоднократно отклоняет такие предложения, заявляя:

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

Комментарии Бена Фойгта помогли мне увидеть непреодолимые проблемы с этим подходом; дано:

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

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

Порядок вычисления подвыражений в любом инициализаторе определяется неопределенным образом [ 1 ]

(Visual C++, gcc и Clang, похоже, имеют согласованное поведение, поскольку все они будут выполнять вызовы в этом порядке:)

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

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

C++ имеет строгие требования к списку инициализаторов 11.6.4[dcl.init.list]4:

В пределах списка инициализатора списка фигурных скобок предложения инициализатора, включая любые, которые являются результатом расширений пакета (17.5.3), оцениваются в порядке, в котором они появляются. Таким образом, каждое вычисление значения и побочный эффект, связанный с данным предложением инициализатора, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным с любым предложением инициализатора, которое следует за ним в списке с разделителями-запятыми списка инициализатора.

Таким образом, поддержка C++ потребовала бы, чтобы это выполнялось в следующем порядке:

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

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

Немного хакерства, так что просто делюсь ради удовольствия.

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

И используйте это как:

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

который расширяется до:

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

Назначенный инициализатор в настоящее время включен в основной текст работы C++20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf чтобы мы могли наконец увидеть их!

В двух основных функциях 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 на самом деле не является постоянной величиной времени компиляции.

Другие вопросы по тегам