Wymuszanie oceny funkcji constexpr w czasie kompilacji
Nie ma sposobu, aby powiedzieć to kompilatorowi funkcja constexpr powinna preferować ocenę w czasie kompilacji, kiedy tylko to możliwe (np. w przypadkach, gdy wartość zwracana przez funkcję constexpr jest używana w wyrażeniu innym niż stałe).
Możemy jednak wymusić funkcję constexpr, która kwalifikuje się do oceny w czasie kompilacji, aby faktycznie oceniała w czasie kompilacji, upewniając się, że wartość zwracana jest używana tam, gdzie wymagane jest wyrażenie stałe. Należy to zrobić dla każdego wywołania.
Najczęstszym sposobem na to jest użycie zwracanej wartości do inicjalizacji zmiennej constexpr (dlatego we wcześniejszych przykładach używaliśmy zmiennej „g”). Niestety, wymaga to wprowadzenia nowej zmiennej do naszego programu, aby zapewnić ocenę czasu kompilacji, co jest brzydkie i zmniejsza czytelność kodu.
Dla zaawansowanych czytelników
Istnieje kilka hackerskich sposobów, które ludzie próbowali obejść problem konieczności wprowadzania nowej zmiennej constexpr za każdym razem, gdy chcemy wymusić ocenę w czasie kompilacji. Zobacz tutaj i tutaj.
Jednak w C++20 istnieje lepsze obejście tego problemu, które za chwilę przedstawimy.
Consteval C++20
C++20 wprowadza słowo kluczowe consteval, które jest używane do wskazania, że funkcja muszą wykonuje ocenę w czasie kompilacji, w przeciwnym razie wystąpi błąd kompilacji przyniesie skutek. Takie funkcje nazywane są funkcjami natychmiastowymi.
#include <iostream>
consteval int greater(int x, int y) // function is now consteval
{
return (x > y ? x : y);
}
int main()
{
constexpr int g { greater(5, 6) }; // ok: will evaluate at compile-time
std::cout << g << '\n';
std::cout << greater(5, 6) << " is greater!\n"; // ok: will evaluate at compile-time
int x{ 5 }; // not constexpr
std::cout << greater(x, 6) << " is greater!\n"; // error: consteval functions must evaluate at compile-time
return 0;
}W powyższym przykładzie pierwsze dwa wywołania greater() zostaną ocenione w czasie kompilacji. Wywołanie greater(x, 6) nie może zostać ocenione w czasie kompilacji, więc spowoduje to błąd kompilacji.
Najlepsza praktyka
Użyj consteval jeśli masz funkcję, która z jakiegoś powodu musi zostać oceniona w czasie kompilacji (np. ponieważ robi coś, co można zrobić tylko w czasie kompilacji).
Być może, co zaskakujące, parametry funkcji constexpr nie są constexpr (mimo że funkcje consteval mogą być oceniane tylko w czasie kompilacji). Tę decyzję podjęto ze względu na spójność.
Określanie, czy wywołanie funkcji constexpr jest oceniane w czasie kompilacji, czy w czasie wykonywania
C++ nie zapewnia obecnie żadnych niezawodnych mechanizmów, aby to zrobić.
Co z std::is_constant_evaluated lub if consteval? Zaawansowane
Żadna z tych możliwości nie informuje, czy wywołanie funkcji jest oceniane w czasie kompilacji, czy runtime.
std::is_constant_evaluated() (zdefiniowane w nagłówku <type_traits>) zwraca bool wskazujące, czy bieżąca funkcja jest wykonywana w kontekście o stałej wartości. kontekst o stałej wartości (zwana także kontekst stały) definiuje się jako taki, w którym wymagane jest wyrażenie stałe (takie jak inicjalizacja zmiennej constexpr). Zatem w przypadkach, gdy kompilator musi ocenić stałe wyrażenie w czasie kompilacji std::is_constant_evaluated() będziesz true zgodnie z oczekiwaniami.
Ma to na celu umożliwienie zrobienia czegoś takiego:
#include <type_traits> // for std::is_constant_evaluated()
constexpr int someFunction()
{
if (std::is_constant_evaluated()) // if evaluating in constant context
doSomething();
else
doSomethingElse();
}Jednak kompilator może również zdecydować się na ocenę funkcji constexpr w czasie kompilacji w kontekście, który nie wymaga stałego wyrażenia. W takich przypadkach std::is_constant_evaluated() zwróci false nawet jeśli funkcja została obliczona w czasie kompilacji. Zatem std::is_constant_evaluated() w rzeczywistości oznacza to, że „kompilator jest zmuszony ocenić to w czasie kompilacji”, a nie „jest to ewaluacja w czasie kompilacji”.
Kluczowa informacja
Chociaż może się to wydawać dziwne, istnieje kilka powodów:
- Jak artykuł, w którym zaproponowano tę funkcję wskazuje, że standard w rzeczywistości nie rozróżnia pomiędzy „czas kompilacji” i „czas wykonywania”. Zdefiniowanie zachowania uwzględniające to rozróżnienie byłoby większą zmianą.
- Optymalizacje nie powinny zmieniać obserwowalnego zachowania programu (chyba że standard wyraźnie na to pozwala). Gdyby
std::is_constant_evaluated()miał wrócićtruekiedy z jakiegokolwiek powodu funkcja została oceniona w czasie kompilacji, wówczas optymalizator decydujący się na ocenę funkcji w czasie kompilacji, a nie w czasie wykonywania, mógłby potencjalnie zmienić obserwowalne zachowanie funkcji. W rezultacie Twój program może zachowywać się bardzo różnie w zależności od poziomu optymalizacji, na jakim został skompilowany!
Chociaż można temu zaradzić na różne sposoby, obejmują one dodanie dodatkowej złożoności do optymalizatora i/lub ograniczenie jego możliwości optymalizacji w niektórych przypadkach.
Wprowadzony w C++ 23, if consteval zastępuje if (std::is_constant_evaluated()) , który zapewnia ładniejszą składnię i naprawia kilka innych problemów. Jednak oblicza to w ten sam sposób.
Używanie constexpr do wykonania constexpr w czasie kompilacji C++20
Wadą funkcji constexpr jest to, że takie funkcje nie mogą oceniać w czasie wykonywania, co czyni je mniej elastycznymi niż funkcje constexpr, które mogą to zrobić. Dlatego nadal przydatny byłby wygodny sposób wymuszania wartościowania funkcji constexpr w czasie kompilacji (nawet jeśli używana jest wartość zwracana, gdy wyrażenie stałe nie jest wymagane), abyśmy mogli jawnie wymusić ocenę w czasie kompilacji, jeśli to możliwe, i ocenę w czasie wykonywania, gdy nie jest to możliwe.
Oto przykład pokazujący, jak to jest możliwe:
#include <iostream>
#define CONSTEVAL(...) [] consteval { return __VA_ARGS__; }() // C++20 version per Jan Scultke (https://stackoverflow.com/a/77107431/460250)
#define CONSTEVAL11(...) [] { constexpr auto _ = __VA_ARGS__; return _; }() // C++11 version per Justin (https://stackoverflow.com/a/63637573/460250)
// This function returns the greater of the two numbers if executing in a constant context
// and the lesser of the two numbers otherwise
constexpr int compare(int x, int y) // function is constexpr
{
if (std::is_constant_evaluated())
return (x > y ? x : y);
else
return (x < y ? x : y);
}
int main()
{
int x { 5 };
std::cout << compare(x, 6) << '\n'; // will execute at runtime and return 5
std::cout << compare(5, 6) << '\n'; // may or may not execute at compile-time, but will always return 5
std::cout << CONSTEVAL(compare(5, 6)) << '\n'; // will always execute at compile-time and return 6
return 0;
}Dla zaawansowanych czytelników
To wykorzystuje variadyczne makro preprocesora ( #define, ..., I __VA_ARGS__), aby zdefiniować stałą lambdę, która jest natychmiast wywoływana (za pomocą nawiasów końcowych).
Informacje na temat makr variadic można znaleźć w https://en.cppreference.com/w/cpp/preprocessor/replace.
Omówimy lambdy w lekcji 20.6 -- Wprowadzenie do lambd (funkcji anonimowych).
Poniższe powinny również działać (i są nieco czystsze, ponieważ nie używają makr preprocesora):
Dla użytkowników gcc
Tam to błąd w GCC 14 i nowszych, który powoduje, że następujący przykład daje błędną odpowiedź, gdy włączony jest dowolny poziom optymalizacji.
#include <iostream>
// Uses abbreviated function template (C++20) and `auto` return type to make this function work with any type of value
// See 'related content' box below for more info (you don't need to know how these work to use this function)
// We've opted to use an uppercase name here for consistency with the prior example, but it also makes it easier to see the call
consteval auto CONSTEVAL(auto value)
{
return value;
}
// This function returns the greater of the two numbers if executing in a constant context
// and the lesser of the two numbers otherwise
constexpr int compare(int x, int y) // function is constexpr
{
if (std::is_constant_evaluated())
return (x > y ? x : y);
else
return (x < y ? x : y);
}
int main()
{
std::cout << CONSTEVAL(compare(5, 6)) << '\n'; // will execute at compile-time
return 0;
}Ponieważ argumenty funkcji constexpr są zawsze w sposób oczywisty oceniane na stałe, jeśli wywołamy funkcję constexpr jako argument funkcji constexpr, ta funkcja constexpr musi zostać obliczona w czasie kompilacji! Funkcja constexpr zwraca następnie wynik funkcji constexpr jako własną wartość zwracaną, tak aby osoba wywołująca mogła z niej skorzystać.
Zauważ, że funkcja constexpr zwraca wartość. Chociaż może to być nieefektywne w czasie wykonywania (jeśli wartość jest jakiegoś typu, którego kopiowanie jest kosztowne, np. std::string), w kontekście czasu kompilacji nie ma to znaczenia, ponieważ całe wywołanie funkcji consteval zostanie po prostu zastąpione obliczoną wartością zwracaną.
Dla zaawansowanych czytelników
Typy automatycznego zwracania omówimy w lekcji 10.9 -- Dedukcja typu dla funkcji.
Omówimy to skrócone szablony funkcji (parametry automatyczne) w lekcji 11.8 -- Szablony funkcji z wieloma typami szablonów 11.8 -- Szablony funkcji z wieloma typami szablonów.

