W poprzednim lekcja 5.5 -- Wyrażenia stałe, zdefiniowaliśmy, czym jest wyrażenie stałe, omówiliśmy, dlaczego wyrażenia stałe są pożądane i zakończyliśmy stwierdzeniem, kiedy wyrażenia stałe faktycznie oceniają czas kompilacji.
W tej lekcji przyjrzymy się bliżej sposobowi tworzenia zmiennych, których można używać w wyrażeniach stałych we współczesnym języku C++. Przyjrzymy się także naszej pierwszej metodzie zapewnienia, że kod faktycznie wykona się w czasie kompilacji.
Wyzwanie podczas kompilacji const Wyzwanie
W poprzedniej lekcji zauważyliśmy, że jednym ze sposobów utworzenia zmiennej, której można użyć w wyrażeniu stałym, jest użycie słowa kluczowego const . W wyrażeniu stałym można zastosować const zmienną typu całkowitego i inicjatorem wyrażenia stałego. Wszystkich pozostałych const zmiennych nie można używać w wyrażeniach stałych.
Jednak użycie const do tworzenia zmiennych, których można używać w wyrażeniach stałych, wiąże się z pewnymi wyzwaniami.
Po pierwsze użycie const nie pozwala od razu stwierdzić, czy zmienna nadaje się do użycia w wyrażeniu stałym, czy nie. W niektórych przypadkach możemy to rozwiązać dość łatwo:
int a { 5 }; // not const at all
const int b { a }; // clearly not a constant expression (since initializer is non-const)
const int c { 5 }; // clearly a constant expression (since initializer is a constant expression)W innych przypadkach może to być dość trudne:
const int d { someVar }; // not obvious whether d is usable in a constant expression or not
const int e { getValue() }; // not obvious whether e is usable in a constant expression or notW powyższym przykładzie zmienne d i e mogą, ale nie muszą, nadawać się do użycia w wyrażeniach stałych, w zależności od sposobu someVar i getValue() zdefiniowania. Oznacza to, że musimy sprawdzić definicje tych inicjatorów i wywnioskować, w jakim przypadku się znajdujemy. A to może nawet nie wystarczyć - jeśli someVar jest const i zainicjalizowane zmienną lub wywołaniem funkcji, będziemy musieli sprawdzić także definicję jego inicjatora!
Po drugie, użycie const nie zapewnia sposobu poinformowania kompilatora, że potrzebujemy zmiennej, która jest można go używać w wyrażeniu stałym (i że powinien zatrzymać kompilację, jeśli tak nie jest). Zamiast tego po prostu utworzy po cichu zmienną, której można użyć tylko w wyrażeniach wykonawczych.
Po trzecie, użycie const do tworzenia stałych zmiennych w czasie kompilacji nie obejmuje zmiennych nieintegralnych. Jest wiele przypadków, w których chcielibyśmy, aby zmienne niecałkowite były również stałymi czasu kompilacji.
Klasa constexpr słowo kluczowe
Na szczęście możemy skorzystać z pomocy kompilatora, aby mieć pewność, że otrzymamy stałą zmienną czasu kompilacji tam, gdzie jej potrzebujemy. Aby to zrobić, używamy słowa kluczowego constexpr (które jest skrótem od „wyrażenia stałego”) zamiast const w deklaracji zmiennej. Zmienna constexpr jest zawsze stałą czasową kompilacji. W rezultacie zmienna constexpr musi zostać zainicjowana wyrażeniem stałym, w przeciwnym razie wystąpi błąd kompilacji.
Na przykład:
#include <iostream>
// The return value of a non-constexpr function is not constexpr
int five()
{
return 5;
}
int main()
{
constexpr double gravity { 9.8 }; // ok: 9.8 is a constant expression
constexpr int sum { 4 + 5 }; // ok: 4 + 5 is a constant expression
constexpr int something { sum }; // ok: sum is a constant expression
std::cout << "Enter your age: ";
int age{};
std::cin >> age;
constexpr int myAge { age }; // compile error: age is not a constant expression
constexpr int f { five() }; // compile error: return value of five() is not constexpr
return 0;
}Ponieważ funkcje zwykle są wykonywane w czasie wykonywania, wartością zwracaną przez funkcję nie jest constexpr (nawet jeśli wyrażenie zwrotne jest wyrażeniem stałym). Dlatego five() nie jest dopuszczalną wartością inicjującą dla constexpr int f.
Powiązana treść
Na lekcji mówimy o funkcjach, których zwracane wartości można wykorzystać w wyrażeniach stałych F.1 -- Funkcje Constexpr.
Dodatkowo constexpr działa dla zmiennych o typach niecałkowych:
constexpr double d { 1.2 }; // d can be used in constant expressions!Znaczenie const vs constexpr dla zmiennych
Dla zmienne:
constoznacza, że po inicjalizacji nie można zmienić wartości obiektu. Wartość inicjatora może być znana w czasie kompilacji lub w czasie wykonywania. Obiekt const może zostać oceniony w czasie wykonywania.constexproznacza, że obiekt może zostać użyty w wyrażeniu stałym. Wartość inicjatora musi być znana w czasie kompilacji. Obiekt constexpr może być oceniany w czasie wykonywania lub kompilacji.
Zmienne Constexpr są domyślnie constexpr. Zmienne const nie są domyślnie constexpr (z wyjątkiem zmiennych całkowitych const z inicjatorem wyrażenia stałego). Chociaż zmienną można zdefiniować jako jedno i drugie constexpr i const, w większości przypadków jest to zbędne i wystarczy użyć jednego const lub constexpr.
W przeciwieństwie do const, constexpr nie jest częścią typu obiektu. Dlatego zmienna zdefiniowana jako constexpr int w rzeczywistości ma typ const int (ze względu na const domyślnie constexpr dostępne dla obiektów).
Najlepsza praktyka
Każda zmienna stała, której inicjatorem jest wyrażenie stałe, powinna być zadeklarowana jako constexpr.
Każda zmienna stała, której inicjator nie jest wyrażeniem stałym (co czyni ją stałą wykonawczą) powinna zostać zadeklarowana as const.
Uwaga: w przyszłości omówimy niektóre typy, które nie są w pełni kompatybilne z constexpr (w tym std::string, std::vector i inne typy, które korzystają z dynamicznej alokacji pamięci). W przypadku obiektów stałych tego typu użyj const zamiast constexpr lub wybierz inny typ, który jest zgodny z constexpr (np. std::string_view lub std::array).
Nomenklatura
Termin constexpr jest kontaminacją „wyrażenia stałego”. Nazwę tę wybrano, ponieważ obiektów constexpr (i funkcji) można używać w wyrażeniach stałych.
Formalnie słowo kluczowe constexpr dotyczy tylko obiektów i funkcji. Tradycyjnie, termin constexpr jest używany jako skrót dla dowolnego wyrażenia stałego (takiego jak 1 + 2).
Nota autora
Niektóre przykłady w tej witrynie zostały napisane przed wprowadzeniem najlepszych praktyk do stosowania constexpr -- w rezultacie można zauważyć, że niektóre przykłady nie są zgodne z powyższą najlepszą praktyką. Obecnie jesteśmy w trakcie aktualizowania niezgodnych przykładów w miarę ich napotykania.
Dla zaawansowanych czytelników
W C i C++ deklaracja obiektu tablicowego (obiektu) może przechowywać wiele wartości) wymaga, aby długość tablicy (liczba wartości, jaką może pomieścić) była znana w czasie kompilacji (aby kompilator mógł zapewnić alokację właściwej ilości pamięci dla obiektów tablicy).
Ponieważ literały są znane w czasie kompilacji, można ich użyć jako długości tablicy:
int arr[5]; // an array of 5 int values, length of 5 is known at compile-timeW wielu przypadkach lepszym rozwiązaniem byłoby użycie stałej symbolicznej jako długości tablicy (np. aby uniknąć magicznych liczb i ułatwić długość tablicy zmienić, jeśli jest używany w wielu miejscach). W C można to zrobić za pomocą makra preprocesora lub modułu wyliczającego, ale nie za pomocą zmiennej const (z wyjątkiem VLA, które mają inne wady). C++, chcąc poprawić tę sytuację, chciał pozwolić na użycie zmiennych const zamiast makr, ale ogólnie zakładano, że wartość zmiennych jest znana tylko w czasie wykonywania, co uniemożliwiało ich użycie jako tablicy. długości.
Aby rozwiązać ten problem, standard języka C++ dodał wyjątek, tak że typy całkowite const z inicjatorem wyrażenia stałego byłyby traktowane jako wartości znane w czasie kompilacji, a zatem można je było wykorzystać jako długości tablicy:
const int arrLen = 5;
int arr[arrLen]; // ok: array of 5 intsKiedy w C++ 11 wprowadzono wyrażenia stałe, sensowne było włączenie do tej definicji stałej int z inicjatorem wyrażenia stałego. Komisja zastanawiała się, czy inne typy powinny być również uwzględnione w tej definicji, ale ostatecznie zdecydowała, że nie to.
Parametry funkcji const i constexpr
Normalne wywołania funkcji są oceniane w czasie wykonywania, a dostarczone argumenty są używane do inicjalizacji parametrów funkcji. Ponieważ inicjalizacja parametrów funkcji odbywa się w czasie wykonywania, prowadzi to do dwóch konsekwencji:
constparametry funkcji są traktowane jako stałe czasu wykonywania (nawet jeśli dostarczony argument jest stałą czasu kompilacji).- Parametry funkcji nie mogą być traktowane jako stałe. należy zadeklarować jako
constexpr, ponieważ ich wartość inicjalizacyjna jest określana dopiero w czasie wykonywania.
Powiązana treść
Omówimy poniżej funkcje, które można obliczyć w czasie kompilacji (a tym samym wykorzystać w wyrażeniach stałych).
C++ obsługuje również sposób przekazywania stałych czasu kompilacji do funkcji. Omawiamy to w lekcji 11.9 — Parametry szablonu inne niż typ.
Nomenklatura podsumowanie
| Termin | Definicja |
|---|---|
| Stała czasowa kompilacji | Wartość lub niemodyfikowalny obiekt, którego wartość musi być znana w czasie kompilacji (np. literały i zmienne constexpr). |
| Constexpr | Słowo kluczowe deklarujące obiekty jako stałe w czasie kompilacji (i funkcje, które mogą być oceniane w czasie kompilacji). Nieformalnie skrót od „wyrażenie stałe”. |
| Wyrażenie stałe | Wyrażenie zawierające tylko stałe czasowe kompilacji i operatory/funkcje obsługujące ocenę w czasie kompilacji. |
| Wyrażenie wykonawcze | Wyrażenie, które nie jest wyrażeniem stałym. |
| Stała czasowa | Wartość lub obiekt niemodyfikowalny, który nie jest stała czasowa kompilacji. |
Krótkie wprowadzenie do funkcji constexpr
A funkcji constexpr to funkcja, którą można wywołać w wyrażeniu stałym. Funkcja constexpr musi być obliczana w czasie kompilacji, podczas gdy wyrażenie stałe, którego jest częścią, musi być obliczane w czasie kompilacji (np. w inicjatorze zmiennej constexpr). W przeciwnym razie funkcja constexpr może zostać oceniona w czasie kompilacji (jeśli jest to dopuszczalne) lub w czasie wykonywania. Aby kwalifikować się do wykonania w czasie kompilacji, wszystkie argumenty muszą być wyrażeniami stałymi.
Aby utworzyć funkcję constexpr, constexpr słowo kluczowe jest umieszczane w deklaracji funkcji przed typem zwracanym:
#include <iostream>
int max(int x, int y) // this is a non-constexpr function
{
if (x > y)
return x;
else
return y;
}
constexpr int cmax(int x, int y) // this is a constexpr function
{
if (x > y)
return x;
else
return y;
}
int main()
{
int m1 { max(5, 6) }; // ok
const int m2 { max(5, 6) }; // ok
constexpr int m3 { max(5, 6) }; // compile error: max(5, 6) not a constant expression
int m4 { cmax(5, 6) }; // ok: may evaluate at compile-time or runtime
const int m5 { cmax(5, 6) }; // ok: may evaluate at compile-time or runtime
constexpr int m6 { cmax(5, 6) }; // okay: must evaluate at compile-time
return 0;
}Nota autora
W tym rozdziale szczegółowo omawialiśmy funkcje constexpr, ale opinie czytelników wskazywały, że temat jest zbyt długi i złożony, aby przedstawić go na początku serii tutoriali. W rezultacie przenieśliśmy całe omówienie z powrotem do lekcji. F.1 -- Funkcje Constexpr.
Najważniejszą rzeczą, którą należy wyciągnąć z tego wprowadzenia, jest to, że funkcję constexpr można wywołać w wyrażeniach stałych.
Funkcje constexpr zostaną użyte w niektórych przyszłych przykładach (w stosownych przypadkach), ale nie będziemy oczekiwać, że zrozumiesz je bliżej lub napiszesz własne funkcje constexpr, dopóki formalnie nie omówimy tematu.

