C ++ 11 umożliwia inicjalizację w klasie elementów niestatycznych i innych niż stałe. Co się zmieniło?

89

Przed C ++ 11 mogliśmy wykonywać inicjalizację w klasie tylko na statycznych elementach stałych typu integralnego lub wyliczeniowego. Stroustrup omawia to w swoim C ++ FAQ , podając następujący przykład:

class Y {
  const int c3 = 7;           // error: not static
  static int c4 = 7;          // error: not const
  static const float c5 = 7;  // error: not integral
};

I następujące rozumowanie:

Dlaczego więc istnieją te niewygodne ograniczenia? Klasa jest zwykle deklarowana w pliku nagłówkowym, a plik nagłówkowy jest zwykle dołączany do wielu jednostek tłumaczeniowych. Jednak aby uniknąć skomplikowanych reguł konsolidatora, C ++ wymaga, aby każdy obiekt miał unikalną definicję. Ta reguła zostałaby złamana, gdyby C ++ pozwolił na definiowanie w klasie jednostek, które musiałyby być przechowywane w pamięci jako obiekty.

Jednak C ++ 11 łagodzi te ograniczenia, umożliwiając inicjalizację w klasie niestatycznych elementów członkowskich (§12.6.2 / 8):

W konstruktorze nie delegującym, jeśli dany niestatyczny element członkowski danych lub klasa bazowa nie jest wyznaczony przez identyfikator-inicjalizatora-pamięci (w tym przypadek, w którym nie ma listy inicjalizatora-pamięci, ponieważ konstruktor nie ma inicjatora-inicjatora ) a więc jednostka nie jest wirtualną klasą bazową klasy abstrakcyjnej (10.4)

  • jeśli jednostka jest niestatycznym składnikiem danych, który ma inicjator nawiasu klamrowego lub równego , jednostka jest inicjowana zgodnie z pkt 8.5;
  • w przeciwnym razie, jeśli jednostka jest składnikiem wariantowym (9.5), inicjalizacja nie jest wykonywana;
  • w przeciwnym razie jednostka jest inicjalizowana domyślnie (8.5).

Sekcja 9.4.2 zezwala również na inicjowanie w klasie niestatycznych elementów członkowskich, jeśli są one oznaczone constexprspecyfikatorem.

Więc co się stało z przyczynami ograniczeń, które mieliśmy w C ++ 03? Czy po prostu akceptujemy „skomplikowane reguły konsolidatora”, czy też zmieniło się coś jeszcze, co ułatwia implementację?

Joseph Mansfield
źródło
5
Nic się nie stało. Kompilatory stały się inteligentniejsze dzięki tym wszystkim szablonom zawierającym tylko nagłówki, więc jest to teraz stosunkowo łatwe rozszerzenie.
Öö Tiib
Co ciekawe, w moim IDE, kiedy wybieram kompilację przed C ++ 11, mogę zainicjować statyczne integralne elementy składowe stałych
Dean P

Odpowiedzi:

67

Krótka odpowiedź jest taka, że ​​zachowali konsolidator mniej więcej taki sam, kosztem uczynienia kompilatora jeszcze bardziej skomplikowanym niż poprzednio.

Oznacza to, że zamiast tego skutkuje wieloma definicjami do uporządkowania przez konsolidator, nadal daje to tylko jedną definicję, a kompilator musi to uporządkować.

Prowadzi to również do nieco bardziej złożonych reguł, które programista musi również uporządkować, ale jest to w większości na tyle proste, że nie jest to wielka sprawa. Dodatkowe reguły pojawiają się, gdy masz dwa różne inicjatory określone dla jednego członka:

class X { 
    int a = 1234;
public:
    X() = default;
    X(int z) : a(z) {}
};

Teraz dodatkowe reguły w tym miejscu dotyczą tego, jaka wartość jest używana do inicjalizacji, agdy używasz konstruktora innego niż domyślny. Odpowiedź na to jest dość prosta: jeśli używasz konstruktora, który nie określa żadnej innej wartości, to 1234zostanie użyty do zainicjowania a- ale jeśli użyjesz konstruktora, który określa inną wartość, to 1234jest w zasadzie ignorowany.

Na przykład:

#include <iostream>

class X { 
    int a = 1234;
public:
    X() = default;
    X(int z) : a(z) {}

    friend std::ostream &operator<<(std::ostream &os, X const &x) { 
        return os << x.a;
    }
};

int main() { 
    X x;
    X y{5678};

    std::cout << x << "\n" << y;
    return 0;
}

Wynik:

1234
5678
Jerry Coffin
źródło
1
Wygląda na to, że wcześniej było to całkiem możliwe. To tylko utrudniało napisanie kompilatora. Czy to uczciwe stwierdzenie?
allyourcode
10
@allyourcode: tak i nie. Tak, utrudniło to pisanie kompilatora. Ale nie, ponieważ utrudniło to również pisanie specyfikacji C ++.
Jerry Coffin
Czy istnieje różnica w sposobie inicjalizacji elementu klasy: int x = 7; lub int x {7} ;?
mbaros
9

Wydaje mi się, że rozumowanie mogło zostać napisane przed sfinalizowaniem szablonów. Przecież wszystkie „skomplikowane reguły linkera” niezbędne dla inicjatorów statycznych w klasie były / były już potrzebne dla C ++ 11 do obsługi statycznych elementów członkowskich szablonów.

Rozważać

struct A { static int s = ::ComputeSomething(); }; // NOTE: This isn't even allowed,
                                                   // thanks @Kapil for pointing that out

// vs.

template <class T>
struct B { static int s; }

template <class T>
int B<T>::s = ::ComputeSomething();

// or

template <class T>
void Foo()
{
    static int s = ::ComputeSomething();
    s++;
    std::cout << s << "\n";
}

Problem dla kompilatora jest taki sam we wszystkich trzech przypadkach: w której jednostce translacyjnej ma emitować definicję si kod niezbędny do jej zainicjowania? Prostym rozwiązaniem jest emitowanie go wszędzie i pozwolenie konsolidatorowi na rozwiązanie tego problemu. Dlatego linkery już obsługiwały takie rzeczy jak __declspec(selectany). Po prostu nie byłoby możliwe zaimplementowanie C ++ 03 bez niego. I dlatego nie było potrzeby rozszerzania linkera.

Mówiąc bardziej dosadnie: myślę, że rozumowanie podane w starym standardzie jest po prostu błędne.


AKTUALIZACJA

Jak zauważył Kapil, mój pierwszy przykład nie jest nawet dozwolony w obecnym standardzie (C ++ 14). I tak zostawiłem, bo to IMO jest najtrudniejszym przypadkiem do implementacji (kompilator, linker). Chodzi mi o to: nawet ten przypadek nie jest trudniejszy niż to, co jest już dozwolone, np. Przy korzystaniu z szablonów.

Paul Groke
źródło
Szkoda, że ​​nie otrzymano żadnych pozytywnych głosów, ponieważ wiele funkcji C ++ 11 jest podobnych, ponieważ kompilatory zawierały już niezbędne możliwości lub optymalizacje.
Alex Court
@AlexCourt Niedawno napisałem tę odpowiedź. Pytanie i odpowiedź Jerry'ego pochodzą jednak z 2012 roku. Więc myślę, że dlatego moja odpowiedź nie wzbudziła większego zainteresowania.
Paul Groke
1
To nie spełni "struct A {static int s = :: ComputeSomething ();}", ponieważ tylko statyczna
stała
8

W teorii So why do these inconvenient restrictions exist?...rozum jest ważny, ale można go raczej łatwo ominąć i to jest dokładnie to, co robi C ++ 11.

Kiedy dołączasz plik, po prostu dołącza plik i ignoruje wszelkie inicjalizacje. Członkowie są inicjowani tylko podczas tworzenia wystąpienia klasy.

Innymi słowy, inicjalizacja jest nadal powiązana z konstruktorem, po prostu notacja jest inna i wygodniejsza. Jeśli konstruktor nie zostanie wywołany, wartości nie zostaną zainicjowane.

Jeśli konstruktor jest wywoływany, wartości są inicjowane z inicjalizacją w klasie, jeśli jest obecna, lub konstruktor może zastąpić to własną inicjalizacją. Ścieżka inicjalizacji jest zasadniczo taka sama, to znaczy za pośrednictwem konstruktora.

Jest to oczywiste z własnego FAQ Stroustrupa dotyczącego C ++ 11.

zar
źródło
Re „Jeśli konstruktor nie zostanie wywołany, wartości nie zostaną zainicjowane”: Jak mogę obejść inicjalizację elementu członkowskiego Y::c3w pytaniu? Jak rozumiem, c3zawsze zostanie zainicjowany, chyba że istnieje konstruktor, który zastępuje wartość domyślną podaną w deklaracji.
Peter - Przywróć Monikę