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 constexpr
specyfikatorem.
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ę?
źródło
Odpowiedzi:
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,
a
gdy 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, to1234
zostanie użyty do zainicjowaniaa
- ale jeśli użyjesz konstruktora, który określa inną wartość, to1234
jest 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
źródło
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ę
s
i 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.
źródło
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.
źródło
Y::c3
w pytaniu? Jak rozumiem,c3
zawsze zostanie zainicjowany, chyba że istnieje konstruktor, który zastępuje wartość domyślną podaną w deklaracji.