W lekcji 1.10 -- Wprowadzenie do wyrażeń, wprowadziliśmy wyrażenia. Domyślnie wyrażenia są obliczane w czasie wykonywania. W niektórych przypadkach muszą to zrobić:
std::cin >> x;
std::cout << 5 << '\n';Ponieważ w czasie kompilacji nie można wykonać operacji wejścia i wyjścia, powyższe wyrażenia są wymagane do obliczenia w czasie wykonywania.
W poprzedniej lekcji 5.4 -- Reguła as-if i optymalizacja czasu kompilacji omówiliśmy regułę „jak gdyby” oraz to, jak kompilator może optymalizować programy, przenosząc pracę ze środowiska wykonawczego na czas kompilacji. Zgodnie z regułą as-if kompilator może wybrać, czy oceniać określone wyrażenia w czasie wykonywania, czy w czasie kompilacji:
const double x { 1.2 };
const double y { 3.4 };
const double z { x + y }; // x + y may evaluate at runtime or compile-timeWyrażenie x + y zwykle oceniałby w czasie wykonywania, ale ponieważ wartość x i y jest znana w czasie kompilacji, kompilator może zdecydować się na wykonanie oceny w czasie kompilacji i zainicjowanie z wartością obliczoną w czasie kompilacji 4.6.
W kilku innych przypadkach język C++ wymaga wyrażenia które można ocenić w czasie kompilacji. Na przykład zmienne constexpr wymagają inicjatora, który można ocenić w czasie kompilacji:
int main()
{
constexpr int x { expr }; // Because variable x is constexpr, expr must be evaluatable at compile-time
}W przypadkach, gdy wymagane jest wyrażenie stałe, ale takie nie zostało podane, kompilator spowoduje błąd i zatrzyma kompilację.
Omówimy zmienne constexpr w następnej lekcji (5.6 -- Zmienne Constexpr), kiedy omówimy constexpr zmienne.
Dla zaawansowanych czytelników
Kilka typowych przypadków, w których wymagane jest wyrażenie ocenialne w czasie kompilacji:
- Inicjator zmiennej constexpr (5.6 -- Zmienne Constexpr).
- Argument szablonu innego typu (11.9 — Parametry szablonu inne niż typ).
- Zdefiniowana długość a
std::array(17.1 — Wprowadzenie do std::array) lub tablicę w stylu C (17.7 -- Wprowadzenie do tablic w stylu C).
W tej lekcji zbadamy więcej możliwości C++ w zakresie oceny w czasie kompilacji i przyjrzymy się, jak C++ odróżnia ten ostatni przypadek od poprzednich dwóch przypadków.
Korzyści z programowania w czasie kompilacji
Chociaż reguła „jak gdyby” doskonale poprawia wydajność, pozostawia nas zależnymi od wyrafinowania oznacza to, że jeśli istnieje sekcja kodu, którą naprawdę chcemy wykonać w czasie kompilacji, może ona zostać wykonana lub nie. Ten sam kod skompilowany na innej platformie lub przy użyciu innego kompilatora lub przy użyciu innych opcji kompilacji lub nieco zmodyfikowany może dać inny wynik, ponieważ zasada „jak gdyby” jest stosowana w sposób niewidoczny, nie otrzymujemy od kompilatora żadnych informacji zwrotnych na temat tego, które części kodu zdecydowały się ocenić w czasie kompilacji ani dlaczego kod ma zostać oceniony w czasie kompilacji może nawet nie być odpowiedni (z powodu literówki lub nieporozumienia) i możemy nigdy się nie dowiedzieć.
Aby poprawić tę sytuację, język C++ umożliwia nam jednoznaczne określenie, które części kodu chcemy wykonać w czasie kompilacji. Użycie funkcji językowych, które skutkują oceną w czasie kompilacji, nazywa się programowaniem w czasie kompilacji.
Te funkcje mogą pomóc w ulepszaniu oprogramowania w wielu przypadkach. obszary:
- Wydajność: Ocena w czasie kompilacji sprawia, że nasze programy są mniejsze i szybsze. Im więcej kodu jesteśmy w stanie ocenić w czasie kompilacji, tym większy korzyści w zakresie wydajności zauważymy.
- Wszechstronność: Zawsze możemy użyć takiego kodu w miejscach, które wymagają wartości w czasie kompilacji. Kod, który opiera się na zasadzie as-if do oceny w czasie kompilacji, nie może być używany w takich miejscach (nawet jeśli kompilator tak postanowi). ocenić ten kod w czasie kompilacji) — tę decyzję podjęto, aby kod, który kompiluje się dzisiaj, nie przestał się kompilować jutro, gdy kompilator zdecyduje się na inną optymalizację.
- Przewidywalność: kompilator może zatrzymać kompilację, jeśli ustali, że kodu nie można wykonać w czasie kompilacji (zamiast cichej decyzji o tym, aby kod był oceniany w czasie wykonywania). Dzięki temu możemy zapewnić, że sekcja kodu, którą naprawdę chcemy wykonać, będzie dostępna w czasie kompilacji.
- Jakość: kompilator może niezawodnie wykrywać określone rodzaje błędów programistycznych w czasie kompilacji i zatrzymywać kompilację, jeśli je napotka. Jest to o wiele bardziej skuteczne niż próba wykrycia i płynnej obsługi tych samych błędów w czasie wykonywania.
- Jakość: Być może najważniejsze, niezdefiniowane zachowanie nie jest dozwolone w czasie kompilacji. Jeśli zrobimy coś, co powoduje niezdefiniowane zachowanie w czasie kompilacji, kompilator powinien zatrzymać kompilację i poprosić nas o naprawienie tego. Należy pamiętać, że jest to trudny problem dla kompilatorów i mogą nie wykryć wszystkich przypadków.
Podsumowując, ocena w czasie kompilacji pozwala nam pisać programy, które są zarówno bardziej wydajne, jak i wyższej jakości (bezpieczniejsze i mniej błędne)! Chociaż zatem ocena w czasie kompilacji dodaje języka dodatkowej złożoności, korzyści mogą być znaczne.
Następujące funkcje C++ są najbardziej podstawowymi programami w czasie kompilacji:
- Zmienne Constexpr (omówione w następnej lekcji 5.6 -- Zmienne Constexpr).
- Funkcje Constexpr (omówione w następnej lekcji F.1 -- Funkcje Constexpr).
- Szablony (przedstawione w lekcji 11.6 -- Szablony funkcji).
- static_assert (omówione w lekcji 9.6 — Assert i static_assert).
Wszystkie te funkcje mają jedną wspólną cechę: wykorzystują wyrażenia stałe.
Być może zaskakujące jest to, że standard C++ prawie w ogóle nie wspomina o „czasie kompilacji”. Zamiast tego standard definiuje „wyrażenie stałe”, które musi być oceniane w czasie kompilacji, wraz z regułami określającymi, w jaki sposób kompilator powinien obsługiwać te wyrażenia. Wyrażenia stałe stanowią podstawę oceny w czasie kompilacji w C++.
W lekcji 1.10 -- Wprowadzenie do wyrażeńzdefiniowaliśmy wyrażenie jako „niepustą sekwencję literałów, zmiennych, operatorów i wywołań funkcji”. A wyrażenie stałe jest niepustą sekwencją literałów, zmiennych stałych, operatorów i wywołań funkcji, z których wszystkie muszą być oceniane w czasie kompilacji. Kluczowa różnica polega na tym, że w wyrażeniu stałym każda część wyrażenia musi być możliwa do oceny w czasie kompilacji.
Kluczowa informacja
W wyrażeniu stałym każda część wyrażenia musi być możliwa do oceny w czasie kompilacji.
Wyrażenie, które nie jest wyrażeniem stałym, jest często nazywane wyrażeniem niestałym i może być nieformalnie nazywane a wyrażeniem wykonawczym (ponieważ takie wyrażenia zwykle są obliczane w czasie wykonywania).
Odczyt opcjonalny
Standard języka C++20 (w sekcji [expr.const]) stwierdza, że „Wyrażenia stałe mogą być oceniane podczas tłumaczenia”. Jak omawialiśmy na lekcji 2.10 — Wprowadzenie do preprocesoratłumaczenie to cały proces tworzenia programu (który obejmuje wstępne przetwarzanie, kompilację i łączenie). Dlatego w skompilowanym programie wyrażenia stałe mogą być oceniane w ramach procesu kompilacji. W interpretowanym programie tłumaczenie odbywa się w czasie wykonywania.
Ponieważ programy w języku C++ są zwykle kompilowane, będziemy postępować przy założeniu, że wyrażenia stałe mogą być oceniane w czasie kompilacji.
Co może znajdować się w wyrażeniu stałym?
Nota autora
Z technicznego punktu widzenia wyrażenia stałe są dość złożone. W tej sekcji przyjrzymy się nieco bliżej temu, co mogą, a czego nie mogą zawierać. Większości z tego nie musisz pamiętać. Jeśli gdzieś wymagane jest wyrażenie stałe, a Ty go nie podasz, kompilator z radością wskaże Twój błąd i będziesz mógł go naprawić w tym momencie.
Najczęściej wyrażenia stałe zawierają następujące elementy:
- Literały (np. „5”, „1.2”)
- Większość operatorów ze stałymi operandami wyrażeń (np.
3 + 4,2 * sizeof(int)). - Stałe zmienne całkowe z inicjatorem wyrażenia stałego (np.
const int x { 5 };). Jest to historyczny wyjątek — we współczesnym C++ preferowane są zmienne constexpr. - Zmienne Constexpr (omówione w następnej lekcji 5.6 -- Zmienne Constexpr).
- Wywołania funkcji Constexpr ze stałymi argumentami wyrażeń (patrz F.1 -- Funkcje Constexpr).
Dla zaawansowanych czytelników
Wyrażenia stałe mogą również zawierać:
- Parametry szablonu nietypowego (patrz 11.9 — Parametry szablonu inne niż typ).
- Liczniki (patrz 13.2 — Wyliczenia bez zakresu).
- Cechy typu (zobacz strona preferencji cpp dla cech typu).
- Wyrażenia lambda Constexpr (patrz 20.6 -- Wprowadzenie do lambd (funkcji anonimowych)).
Wskazówka
Warto zauważyć, że w wyrażeniu stałym nie można używać następujących elementów:
- Zmienne inne niż stałe.
- Stałe zmienne niecałkowite, nawet jeśli mają stały inicjator wyrażenia (np.
const double d { 1.2 };). Aby użyć takich zmiennych w wyrażeniu stałym, zdefiniuj je zamiast tego jako zmienne constexpr (patrz lekcja 5.6 -- Zmienne Constexpr). - Wartości zwracane przez funkcje inne niż constexpr (nawet jeśli wyrażenie zwrotne jest wyrażeniem stałym).
- Parametry funkcji (nawet jeśli funkcją jest constexpr).
- Operatory z operandami, które nie są wyrażeniami stałymi (np.
x + ygdyxlubynie jest wyrażeniem stałym, lubstd::cout << "hello\n"jakstd::coutnie jest wyrażeniem stałym). - Operatorzy
new,delete,throw,typeid, Ioperator,(przecinek).
Wyrażenie zawierające którekolwiek z powyższych jest wyrażeniem środowiska wykonawczego.
Powiązana treść
Dokładną definicję wyrażenia stałego można znaleźć w artykule strona cpppreference dla wyrażeń stałych. Należy zauważyć, że wyrażenie stałe jest definiowane przez rodzaj wyrażenia, którym nie jest. Oznacza to, że pozostaje nam wywnioskować, co to jest. Powodzenia!
Nomenklatura
Omawiając wyrażenia stałe, często używa się jednego z dwóch sformułowań:
- „X można zastosować w wyrażeniu stałym” jest często używane, gdy podkreślamy, czym jest X. np. „
5nadaje się do użycia w wyrażeniu stałym” podkreśla, że dosłowność5można używać w wyrażeniu stałym. - „X jest wyrażeniem stałym” jest czasami używane, gdy podkreśla się, że pełne wyrażenie (składające się z X) jest wyrażeniem stałym. np. „
5jest wyrażeniem stałym” podkreśla, że wyrażenie5jest wyrażeniem stałym.
To drugie może brzmieć niezręcznie, gdy zostanie sformułowane w stylu „literały są wyrażeniami stałymi” (ponieważ w rzeczywistości są wartościami). Oznacza to jednak po prostu, że wyrażenie składające się z literału jest wyrażeniem stałym.
Na marginesie…
Kiedy zdefiniowano wyrażenia stałe, const typy integralne zostały przyjęte, ponieważ były już traktowane jako wyrażenia stałe w języku.
Komisja dyskutowała, czy const typy nieintegralne z inicjatorem wyrażenia stałego powinny być również traktowane jako wyrażenia stałe (dla spójności z const przypadek typów całkowych). Ostatecznie zdecydowali się tego nie robić, aby promować bardziej spójne użycie constexpr.
Przykłady wyrażeń stałych i niestałych
W poniższym programie przyjrzymy się niektórym instrukcjom wyrażeń i określimy, czy każde wyrażenie jest wyrażeniem stałym, czy wyrażeniem wykonawczym:
#include <iostream>
int getNumber()
{
std::cout << "Enter a number: ";
int y{};
std::cin >> y; // can only execute at runtime
return y; // this return expression is a runtime expression
}
// The return value of a non-constexpr function is a runtime expression
// even when the return expression is a constant expression
int five()
{
return 5; // this return expression is a constant expression
}
int main()
{
// Literals can be used in constant expressions
5; // constant expression
1.2; // constant expression
"Hello world!"; // constant expression
// Most operators that have constant expression operands can be used in constant expressions
5 + 6; // constant expression
1.2 * 3.4; // constant expression
8 - 5.6; // constant expression (even though operands have different types)
sizeof(int) + 1; // constant expression (sizeof can be determined at compile-time)
// The return values of non-constexpr functions can only be used in runtime expressions
getNumber(); // runtime expression
five(); // runtime expression (even though the return expression is a constant expression)
// Operators without constant expression operands can only be used in runtime expressions
std::cout << 5; // runtime expression (std::cout isn't a constant expression operand)
return 0;
}W poniższym fragmencie definiujemy kilka zmiennych i wskazujemy, czy można ich używać w wyrażeniach stałych:
// Const integral variables with a constant expression initializer can be used in constant expressions:
const int a { 5 }; // a is usable in constant expressions
const int b { a }; // b is usable in constant expressions (a is a constant expression per the prior statement)
const long c { a + 2 }; // c is usable in constant expressions (operator+ has constant expression operands)
// Other variables cannot be used in constant expressions (even when they have a constant expression initializer):
int d { 5 }; // d is not usable in constant expressions (d is non-const)
const int e { d }; // e is not usable in constant expressions (initializer is not a constant expression)
const double f { 1.2 }; // f is not usable in constant expressions (not a const integral variable)Gdy wyrażenia stałe są oceniane w czasie kompilacji
Ponieważ wyrażenia stałe zawsze można oceniać w czasie kompilacji, można założyć, że wyrażenia stałe będą zawsze oceniane w czasie kompilacji. Wbrew intuicji tak nie jest.
Kompilator jest wymagany tylko do oceniania wyrażeń stałych w czasie kompilacji w kontekstach, które wymagają wyrażenie stałe.
Nomenklatura
Techniczna nazwa wyrażenia, które musi być oceniane w czasie kompilacji, to wyrażenie w sposób oczywisty o stałej wartości . Prawdopodobnie spotkasz się z tym terminem tylko w dokumentacji technicznej.
W kontekstach, które nie wymagają wyrażenia stałego, kompilator może wybrać, czy ma oceniać wyrażenie stałe w czasie kompilacji, czy w czasie wykonywania.
const int x { 3 + 4 }; // constant expression 3 + 4 must be evaluated at compile-time
int y { 3 + 4 }; // constant expression 3 + 4 may be evaluated at compile-time or runtimeZmienna x ma typ const int a inicjator wyrażenia stałego x można zastosować w wyrażeniu stałym. Jego inicjator musi zostać oceniony w czasie kompilacji (w przeciwnym razie wartość x nie byłaby znana w czasie kompilacji i x nie byłaby użyteczna w wyrażeniu stałym). Z drugiej strony zmienna y nie jest stałą, więc y nie można jej używać w wyrażeniu stałym. Mimo że jego inicjatorem jest wyrażenie stałe, kompilator może zdecydować się na ocenę inicjatora w czasie kompilacji lub w czasie wykonywania.
Nawet jeśli nie jest to wymagane, nowoczesne kompilatory Zazwyczaj oceniają stałe wyrażenie w czasie kompilacji, gdy włączone są optymalizacje.
Kluczowa informacja
Kompilator jest wymagany tylko do oceniania wyrażeń stałych w czasie kompilacji w kontekstach, które wymagają wyrażenia stałego. Może tak się zdarzyć w innych przypadkach.
Wskazówka
Prawdopodobieństwo, że wyrażenie zostanie w pełni ocenione w czasie kompilacji, można podzielić na następujące kategorie:
- Nigdy: niestałe wyrażenie, w którym kompilator nie jest w stanie określić wszystkich wartości w czasie kompilacji.
- Możliwie: wyrażenie niestałe, w którym kompilator jest w stanie określić wszystkie wartości w czasie kompilacji (zoptymalizowane pod zasada as-if).
- Prawdopodobnie: Stałe wyrażenie używane w kontekście, który nie wymaga stałego wyrażenia.
- Zawsze: Stałe wyrażenie używane w kontekście, który wymaga stałego wyrażenia.
Dla zaawansowanych czytelników
Dlaczego więc C++ nie wymaga, aby wszystkie wyrażenia stałe były oceniane w czasie kompilacji? Są co najmniej dwa dobre powody:
- Ocena w czasie kompilacji utrudnia debugowanie. Jeśli nasz kod zawiera błędne obliczenia, które są oceniane w czasie kompilacji, mamy ograniczone narzędzia do diagnozowania problemu. Zezwolenie na ocenę niewymaganych wyrażeń stałych w czasie wykonywania (zwykle gdy optymalizacje są wyłączone) umożliwia debugowanie naszego kodu w czasie wykonywania. Możliwość przeglądania i sprawdzania stanu naszych programów podczas ich działania może ułatwić znajdowanie błędów.
- Aby zapewnić kompilatorowi elastyczność w zakresie optymalizacji według własnego uznania (lub pod wpływem opcji kompilatora). Na przykład kompilator może chcieć zaoferować opcję opóźniającą ocenę wszystkich niewymaganych wyrażeń stałych do czasu wykonania, aby skrócić czas kompilacji dla programistów.
Dlaczego wyrażenia w czasie kompilacji muszą być stałe Opcjonalne
Możesz się zastanawiać, dlaczego wyrażenia w czasie kompilacji mogą zawierać tylko obiekty stałe (oraz operatory i funkcje, które mogą obliczać stałe w czasie kompilacji).
Rozważ następujący program:
#include <iostream>
int main()
{
int x { 5 };
// x is known to the compiler at this point
std::cin >> x; // read in value of x from user
// x is no longer known to the compiler
return 0;
}Aby początek, x jest inicjalizowany wartością 5. Wartość x jest w tym momencie znana kompilatorowi. Ale wtedy x przypisuje się wartość od użytkownika. Kompilator nie może wiedzieć, jaką wartość poda użytkownik w czasie kompilacji, więc poza tym punktem wartość x nie jest znana kompilatorowi. Zatem wyrażenie x nie zawsze daje się ocenić w czasie kompilacji, co narusza wymaganie, że takie wyrażenie musi zawsze dać się ocenić w czasie kompilacji.
Ponieważ stałe nie mogą mieć zmienianych wartości, stała zmienna, której inicjator jest oceniany w czasie kompilacji, zawsze będzie miała wartość znaną w czasie kompilacji. Dzięki temu wszystko jest proste.
Chociaż projektanci języka mogli zdefiniować wyrażenie w czasie kompilacji jako takie, którego wszystkie wartości są aktualnie znane w czasie kompilacji (a nie wyrażenie, które zawsze musi dać się ocenić w czasie kompilacji), spowodowałoby to znaczną złożoność kompilatora (ponieważ kompilator byłby teraz odpowiedzialny za określenie, kiedy każdą zmienną można zmienić na wartość nieznaną w czasie kompilacji). Dodanie pojedynczej linii kodu (np std::cin >> x) może przerwać program w innym miejscu (jeśli x był używany w dowolnym kontekście wymagającym wartości znanej w czasie kompilacji).
Czas quizu
Pytanie nr 1
Dla każdego stwierdzenia określ:
- Określa, czy inicjator jest wyrażeniem stałym, czy wyrażeniem niestałym.
- Określa, czy zmienna jest wyrażeniem stałym czy niestałym.
A)
char a { 'q' };B)
const int b { 0 };C)
const double c { 5.0 };D)
const int d { a * 2 }; // a defined as char a { 'q' };mi)
int e { c + 1.0 }; // c defined as const double c { 5.0 };f)
const int f { d * 2 }; // d defined as const int d { 0 };g)
const int g { getNumber() }; // getNumber returns an int by valueh)
Dodatkowe punkty:
const int h{};
