Dlaczego C ++ 11 nie obsługuje wyznaczonych list inicjalizujących jako C99? [Zamknięte]

121

Rozważać:

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

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

Powyższy kod jest legalny w C99, ale niedozwolony w C ++ 11.

Co to było uzasadnienie komisji standaryzacyjnej, aby wykluczyć wsparcie dla tak przydatnej funkcji?

xmllmx
źródło
10
Najwyraźniej nie było sensu włączać go do komitetu projektowego, albo po prostu nie pojawiało się to na spotkaniach. Warto zauważyć, że inicjatory wyznaczone przez C99 nie znajdują się w żadnej z wersji specyfikacji C ++. Konstruktory wydają się być preferowaną konstrukcją inicjalizacyjną i nie bez powodu: gwarantują spójną inicjalizację obiektów, jeśli napiszesz je poprawnie.
Robert Harvey,
19
Twoje rozumowanie jest zacofane, język nie musi mieć uzasadnienia, aby nie mieć cechy, potrzebuje uzasadnienia, aby ją mieć i do tego mocnego. C ++ jest wystarczająco nadęty, w obecnej postaci.
Matthieu M.,
42
Dobrym powodem (którego nie da się rozwiązać za pomocą konstruktorów inaczej niż przez pisanie oszałamiających opakowań) jest to, że niezależnie od tego, czy używasz C ++, czy nie, większość prawdziwych interfejsów API to C, a nie C ++, a nieliczne z nich sprawiają, że dostarczasz strukturę, w której chcesz ustawić jedno lub dwa pola - i niekoniecznie pierwsze - ale pozostałe muszą mieć inicjalizację zerową. OVERLAPPEDTakim przykładem jest Win32 API . Możliwość pisania ={.Offset=12345};sprawiłaby, że kod byłby znacznie bardziej przejrzysty (i prawdopodobnie mniej podatny na błędy). Podobnym przykładem są gniazda BSD.
Damon
14
Kod w mainC99 jest niezgodny z prawem. Powinien brzmieć struct Person p = { .age = 18 };
chqrlie
14
FYI C ++ 20 będzie obsługiwał wyznaczone inicjatory
Andrew Tomazos

Odpowiedzi:

34

C ++ ma konstruktory. Jeśli ma sens zainicjowanie tylko jednego elementu członkowskiego, można to wyrazić w programie za pomocą odpowiedniego konstruktora. To rodzaj abstrakcji promowanej przez C ++.

Z drugiej strony wyznaczona funkcja inicjatorów polega bardziej na ujawnianiu i ułatwianiu dostępu do członków bezpośrednio w kodzie klienta. Prowadzi to do takich rzeczy, jak posiadanie osoby w wieku 18 (lat?), Ale o zerowym wzroście i wadze.


Innymi słowy, wyznaczone inicjatory obsługują styl programowania, w którym elementy wewnętrzne są ujawniane, a klient ma elastyczność w decydowaniu, w jaki sposób chce używać tego typu.

C ++ jest bardziej zainteresowany umieszczeniem elastyczności po stronie projektanta typu, więc projektanci mogą ułatwić prawidłowe użycie typu i utrudnić jego niepoprawne użycie. Zapewnienie projektantowi kontroli nad sposobem inicjalizacji typu jest częścią tego: projektant określa konstruktory, inicjatory w klasie itp.

bames53
źródło
12
Pokaż odsyłacz do tego, co według Ciebie jest powodem, dla którego C ++ nie ma wyznaczonych inicjatorów. Nie pamiętam, abym kiedykolwiek widział propozycję tego.
Johannes Schaub - litb
20
Czy to nie powód braku konstruktora Person, którego autor chciał zapewnić użytkownikom jak największą elastyczność w ustawianiu i inicjowaniu elementów członkowskich? Użytkownik może też już pisać Person p = { 0, 0, 18 };(i nie bez powodu).
Johannes Schaub - litb
7
Coś podobnego zostało ostatnio zaakceptowane w specyfikacji C ++ 14 przez open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3605.html .
Johannes Schaub - litb
4
@ JohannesSchaub-litb Nie mówię o czysto mechanicznej, bezpośredniej przyczynie (tj. Nie została ona zgłoszona komisji). Opisuję to, co uważam za dominujący czynnik. - Personma konstrukcję bardzo C, więc funkcje C mogą mieć sens. Jednak C ++ prawdopodobnie umożliwia lepszy projekt, który również eliminuje potrzebę wyznaczonych inicjatorów. - Moim zdaniem usunięcie ograniczenia dotyczącego inicjatorów w klasie dla agregatów jest znacznie bardziej zgodne z etosem C ++ niż wyznaczonych inicjatorów.
bames53,
4
Zamiast tego w C ++ można by nazwać argumenty funkcji. Ale w tej chwili argumenty nazw oficjalnie nie istnieją. Zobacz N4172 Nazwane argumenty, aby zapoznać się z propozycją tego. Dzięki temu kod byłby mniej podatny na błędy i łatwiejszy do odczytania.
David Baird
89

15 lipca 2017 P0329R4 został przyjęty dostandard: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf
Zapewnia to ograniczone wsparcie dlaWyznaczone inicjatory. To ograniczenie opisano poniżej w C.1.7 [diff.decl] .4, biorąc pod uwagę:

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

Następujące wyznaczone inicjalizacje, które są ważne w języku C, są ograniczone w języku C ++:

  • struct A a = { .y = 1, .x = 2 } jest nieprawidłowy w C ++, ponieważ desygnatory muszą występować w kolejności deklaracji członków danych
  • int arr[3] = { [1] = 5 } jest nieprawidłowy w C ++, ponieważ inicjalizacja wyznaczona przez tablicę nie jest obsługiwana
  • struct B b = {.a.x = 0} jest nieprawidłowy w C ++, ponieważ desygnatory nie mogą być zagnieżdżane
  • struct A c = {.x = 1, 2} jest nieprawidłowy w C ++, ponieważ wszystkie lub żadne elementy członkowskie danych nie muszą być inicjowane przez desygnatory

Dla a wcześniej Boost faktycznie obsługuje Desygnowanych Intializatorów i było wiele propozycji dodania obsługistandard, na przykład: n4172 i propozycja Daryle Walkera, aby dodać oznaczenie do inicjatorów . Propozycje dotyczą implementacjiWyznaczone inicjatory w Visual C ++, gcc i Clang:

Uważamy, że zmiany będą stosunkowo łatwe do wdrożenia

Ale komisja standaryzacyjna wielokrotnie odrzuca takie propozycje , stwierdzając:

EWG znalazł różne problemy z proponowanym podejściem i nie sądził, że jest to wykonalne, aby spróbować rozwiązać problem, ponieważ było to wielokrotnie próbowane i za każdym razem kończyło się niepowodzeniem

Komentarze Bena Voigta pomogły mi dostrzec nie do przezwyciężenia problemy związane z tym podejściem; dany:

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

W jakiej kolejności byłyby wywoływane te funkcje : struct X foo = {.a = (char)f(), .b = g(), .c = h()}? Co zaskakujące, w:

Kolejność oceny podwyrażeń w dowolnym inicjatorze jest nieokreślona [ 1 ]

( Wygląda na to, że Visual C ++, gcc i Clang mają uzgodnione zachowanie, ponieważ wszystkie będą wykonywać wywołania w tej kolejności :)

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

Ale nieokreślony charakter standardu oznacza, że ​​gdyby te funkcje miały jakąkolwiek interakcję, wynikowy stan programu również byłby nieokreślony, a kompilator nie ostrzegłby cię : Czy istnieje sposób, aby ostrzec o niewłaściwym działaniu wyznaczonych inicjatorów?

nie mają rygorystyczne wymogi inicjalizator-List 11.6.4 [dcl.init.list] 4:

W ramach listy inicjalizacyjnej listy inicjalizacyjnej, klauzule inicjalizujące, w tym wszystkie wynikające z rozszerzeń pakietów (17.5.3), są oceniane w kolejności, w jakiej się pojawiają. Oznacza to, że każde obliczenie wartości i efekt uboczny skojarzony z daną klauzulą ​​inicjalizatora jest sekwencjonowany przed każdym obliczeniem wartości i efektem ubocznym związanym z każdą klauzulą ​​inicjalizatora, która następuje po niej, na liście rozdzielanej przecinkami listy inicjalizatora.

Więc wsparcie wymagałoby wykonania tego w kolejności:

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

Łamanie kompatybilności z poprzednimi wersjami wdrożenia.
Jak omówiono powyżej, omijano ten problem przez ograniczenia dotyczące wyznaczonych inicjatorów zaakceptowanych w programie. Zapewniają ustandaryzowane zachowanie, gwarantując kolejność wykonywania Wyznaczonych inicjatorów.

Jonathan Mee
źródło
3
Oczywiście, w tym kodzie: struct X { int c; char a; float b; }; X x = { .a = f(), .b = g(), .c = h() };wywołaniu h()odbywa się przed albo f()albo g(). Jeśli definicja struct Xnie jest w pobliżu, będzie to bardzo zaskakujące. Pamiętaj, że wyrażenia inicjujące nie muszą być wolne od skutków ubocznych.
Ben Voigt
2
Oczywiście nie jest to nic nowego, inicjalizacja elementu ctor już ma ten problem, ale jest to w definicji elementu klasy, więc ścisłe powiązanie nie jest zaskoczeniem. Wyznaczone inicjatory nie mogą odwoływać się do innych członków w sposób, w jaki mogą to inicjować elementy członkowskie ctor.
Ben Voigt
2
@MattMcNabb: Nie, nie jest bardziej ekstremalne. Ale oczekuje się, że programista implementujący konstruktor klasy będzie znał kolejność deklaracji elementów członkowskich. Natomiast konsumentem klasy może być zupełnie inny programista. Ponieważ chodzi o to, aby umożliwić inicjalizację bez konieczności sprawdzania kolejności członków, wydaje się to fatalnym błędem w propozycji. Ponieważ wyznaczone inicjatory nie mogą odwoływać się do konstruowanego obiektu, pierwsze wrażenie jest takie, że wyrażenia inicjujące mogą być oceniane najpierw w kolejności oznaczania, a następnie inicjalizacja elementu członkowskiego w kolejności deklaracji. Ale ...
Ben Voigt
2
@JonathanMee: Cóż, drugie pytanie odpowiadało, że ... inicjatory agregatu C99 są nieuporządkowane, więc nie oczekuje się zamówienia wyznaczonych inicjatorów. C ++ braced-init-lists SĄ uporządkowane, a propozycja dla wyznaczonych inicjatorów używa potencjalnie zaskakującej kolejności (nie możesz być spójny zarówno z porządkiem leksykalnym, używanym dla wszystkich list braced-init, jak i kolejnością elementów, używaną do inicjalizacji ctor -listy)
Ben Voigt
3
Jonathan: „Obsługa c ++ wymagałaby wykonania tego w kolejności [...] Zerwanie zgodności z poprzednimi implementacjami c99”. Nie rozumiem tego, przepraszam. 1. Jeśli kolejność jest nieokreślona w C99, to oczywiście każde rzeczywiste zamówienie powinno być w porządku, w tym dowolny dowolny wybór w C ++. b) Brak obsługi des. inicjatory w ogóle już trochę psują kompatybilność z C99 ...
Sz.
34

Trochę włamań, więc po prostu udostępniaj dla zabawy.

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

I używaj go jak:

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

który rozszerza się do:

MyFunction(([&] {
 Params ${};
 $.Name = "Foo Bar", $.Age = 18;
 return $;
}()));
keebus
źródło
Neat, tworzy lambdę ze zmienną o nazwie $type Ti przypisujesz jej elementy członkowskie bezpośrednio przed jej zwróceniem. Ładne. Zastanawiam się, czy są z tym jakieś problemy z wydajnością.
TankorSmash
1
W zoptymalizowanej kompilacji nie widać śladów lambdy ani jej wywołania. Wszystko jest podszyte.
keebus
1
Bardzo mi się podoba ta odpowiedź.
Seph Reed
6
Woah. Nawet nie wiedziałem, że $ to prawidłowe imię.
Chris Watts,
Był obsługiwany przez starsze kompilatory C, a wsparcie zostało zachowane dla kompatybilności wstecznej.
keebus
22

Wyznaczone inicjatory są obecnie zawarte w pracy C ++ 20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0329r4.pdf, więc być może w końcu je zobaczymy!

Siergiej A.
źródło
3
Ale zwróć uwagę, że są one ograniczone: w C ++ wyznaczona obsługa inicjalizacji jest ograniczona w porównaniu z odpowiadającą jej funkcjonalnością w C. W C ++ desygnatory dla niestatycznych składowych danych muszą być określone w kolejności deklaracji, desygnatory dla elementów tablicy i zagnieżdżonych desygnatorów nie są obsługiwane, a wyznaczone i niewyznaczone inicjatory nie mogą być mieszane na tej samej liście inicjatorów. Oznacza to, że w szczególności nadal nie będzie można łatwo utworzyć tabeli odnośników z kluczem wyliczeniowym .
Ruslan
@Ruslan: Zastanawiam się, dlaczego C ++ tak bardzo je ograniczył? Rozumiem, że może być niejasność co do tego, czy kolejność, w jakiej wartości elementów są oceniane i / lub zapisywane do struktury, jest zgodna z kolejnością, w której elementy są określone na liście inicjalizacji, lub w jakiej kolejności pojawiają się elementy w strukturze, ale rozwiązaniem tego byłoby po prostu stwierdzenie, że wyrażenia inicjujące są wykonywane w dowolnej kolejności, a czas życia obiektu nie zaczyna się, dopóki inicjalizacja nie zostanie zakończona ( &operator zwróciłby adres, który obiekt będzie miał podczas swojego życia).
supercat
5

Dwie podstawowe funkcje C99, których w C ++ 11 brakuje, wymienia „Wyznaczone inicjatory i C ++”.

Myślę, że „wyznaczony inicjator” związany jest z potencjalną optymalizacją. Tutaj jako przykładu używam „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();
}

Wiedzieliśmy w czasie kompilacji, że a_point.xwynosi zero, więc mogliśmy się spodziewać, że foozostanie zoptymalizowany do jednego 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"

foojest zoptymalizowany x == 0tylko do drukowania .

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

I to jest wyjście zoptymalizowanego kodu asemblera.

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   

Widzimy, że a_pointtak naprawdę nie jest to stała czasowa kompilacji.

wcy
źródło
8
Teraz spróbuj constexpr point(int _x,int _y):x(_x),y(_y){}. Optymalizator clang ++ wydaje się eliminować również porównanie w kodzie. Więc to tylko kwestia QoI.
dyp
Spodziewałbym się również, że cały obiekt a_point zostałby zoptymalizowany, gdyby miał wewnętrzne powiązanie. tj. umieść go w anonimowej przestrzeni nazw i zobacz, co się stanie. goo.gl/wNL0HC
Arvid,
@dyp: Nawet samo zdefiniowanie konstruktora jest możliwe tylko wtedy, gdy typ jest pod twoją kontrolą. Nie możesz tego zrobić, na przykład za struct addrinfolub struct sockaddr_in, więc zostajesz z zadaniami odrębnymi od deklaracji.
musiphil
2
@musiphil Przynajmniej w C ++ 14 te struktury w stylu C mogą być poprawnie ustawione w funkcji constexpr jako zmienne lokalne za pomocą przypisania, a następnie zwrócone z tej funkcji. Dodatkowo nie chodziło mi o pokazanie alternatywnej implementacji konstruktora w C ++, która pozwala na optymalizację, ale pokazanie, że kompilator jest w stanie wykonać tę optymalizację, jeśli forma inicjalizacji jest inna. Jeśli kompilator jest „dostatecznie dobry” (tj. Obsługuje tę formę optymalizacji), to nie powinno być istotne, czy używasz ctora, wyznaczonych inicjatorów, czy czegoś innego.
dyp