F.1 -- Funkcje Constexpr

W lekcji 5.6 -- Zmienne Constexpr, wprowadziliśmy słowo kluczowe constexpr , które używaliśmy do tworzenia stałych (symbolicznych) czasu kompilacji. Wprowadziliśmy także wyrażenia stałe, które można oceniać w czasie kompilacji, a nie w czasie wykonywania.

Jednym z wyzwań związanych z wyrażeniami stałymi jest to, że wywołania funkcji normalnej nie są dozwolone w wyrażeniach stałych. Oznacza to, że nie możemy używać takich wywołań funkcji wszędzie tam, gdzie wymagane jest wyrażenie stałe.

Rozważ następujący program:

#include <iostream>

int main()
{
    constexpr double radius { 3.0 };
    constexpr double pi { 3.14159265359 };
    constexpr double circumference { 2.0 * radius * pi };
    
    std::cout << "Our circle has circumference " << circumference << "\n";

    return 0;    
}

Daje to wynik:

Our circle has circumference 18.8496

Posiadanie złożonego inicjatora dla circumference nie jest zbyt dobre (i wymaga utworzenia instancji dwóch zmiennych pomocniczych, radius i pi). Zróbmy więc z tego funkcję:

#include <iostream>

double calcCircumference(double radius)
{
    constexpr double pi { 3.14159265359 };
    return 2.0 * pi * radius;
}

int main()
{
    constexpr double circumference { calcCircumference(3.0) }; // compile error
    
    std::cout << "Our circle has circumference " << circumference << "\n";

    return 0;    
}

Ten kod jest znacznie czystszy. Również się nie kompiluje. Zmienna Constexpr circumference wymaga, aby jej inicjator był wyrażeniem stałym, a wywołanie calcCircumference() nie jest wyrażeniem stałym.

W tym konkretnym przypadku moglibyśmy wykonać circumference nie constexpr, a program się skompiluje. Stracilibyśmy korzyści płynące z wyrażeń stałych, ale przynajmniej program by działał.

Jednak w C++ są inne przypadki (które przedstawimy w przyszłości), w których nie mamy dostępnych alternatywnych opcji i wystarczy tylko wyrażenie stałe. W takich przypadkach naprawdę chcielibyśmy móc korzystać z funkcji, ale wywołania normalnych funkcji po prostu nie będą działać. Co więc mamy zrobić?

Funkcji Constexpr można używać w wyrażeniach stałych

A funkcji constexpr to funkcja, którą można wywoływać w wyrażeniu stałym.

Aby uczynić funkcję funkcją constexpr, po prostu używamy słowa kluczowego constexpr przed powrotem funkcji type.

Kluczowa informacja

Klasa constexpr słowo kluczowe służy do sygnalizowania kompilatorowi i innym programistom, że funkcja może zostać użyta w wyrażeniu stałym.

Oto ten sam przykład co powyżej, ale z użyciem funkcji constexpr:

#include <iostream>

constexpr double calcCircumference(double radius) // now a constexpr function
{
    constexpr double pi { 3.14159265359 };
    return 2.0 * pi * radius;
}

int main()
{
    constexpr double circumference { calcCircumference(3.0) }; // now compiles
    
    std::cout << "Our circle has circumference " << circumference << "\n";

    return 0;    
}

Ponieważ calcCircumference() jest teraz funkcją constexpr, można jej używać w wyrażeniu stałym, takim jak inicjator circumference.

Funkcje Constexpr można oceniać podczas kompilacji time

W lekcji 5.5 -- Wyrażenia stałe zauważyliśmy, że w kontekstach wymagających wyrażenia stałego (takich jak inicjalizacja zmiennej constexpr) wymagane jest wyrażenie stałe do obliczenia w czasie kompilacji. Jeśli wymagane wyrażenie stałe zawiera wywołanie funkcji constexpr, to wywołanie funkcji constexpr musi zostać obliczone w czasie kompilacji.

W naszym przykładzie powyżej zmienna circumference jest constexpr i dlatego wymaga inicjatora wyrażenia stałego. Ponieważ calcCircumference() jest częścią wymaganego wyrażenia stałego, calcCircumference() musi zostać obliczone w czasie kompilacji.

Gdy wywołanie funkcji jest oceniane w czasie kompilacji, kompilator obliczy wartość zwracaną przez wywołanie funkcji w czasie kompilacji, a następnie zastąpi wywołanie funkcji wartością zwracaną.

Więc w naszym przykładzie wywołanie calcCircumference(3.0) zostanie zastąpione wynikiem wywołanie funkcji, czyli 18.8496. Innymi słowy, kompilator to skompiluje:

#include <iostream>

constexpr double calcCircumference(double radius)
{
    constexpr double pi { 3.14159265359 };
    return 2.0 * pi * radius;
}

int main()
{
    constexpr double circumference { 18.8496 };
    
    std::cout << "Our circle has circumference " << circumference << "\n";

    return 0;    
}

Aby obliczyć w czasie kompilacji, muszą być również spełnione dwie inne rzeczy:

  • Wywołanie funkcji constexpr musi mieć argumenty znane w czasie kompilacji (np. są wyrażeniami stałymi).
  • Wszystkie instrukcje i wyrażenia w funkcji constexpr muszą być oceniane w czasie kompilacji.

Gdy a constexpr (lub consteval) jest oceniana w czasie kompilacji, wszelkie inne wywoływane przez nią funkcje muszą być oceniane w czasie kompilacji (w przeciwnym razie funkcja początkowa nie byłaby w stanie zwrócić wyniku w czasie kompilacji).

Dla zaawansowanych czytelników

Istnieją także inne, rzadziej spotykane kryteria. Można je znaleźć tutaj.

Funkcje Constexpr można również ocenić w czasie wykonywania

Funkcje Constexpr można także oceniać w czasie wykonywania i w takim przypadku zwrócą wynik inny niż constexpr. Na przykład:

#include <iostream>

constexpr int greater(int x, int y)
{
    return (x > y ? x : y);
}

int main()
{
    int x{ 5 }; // not constexpr
    int y{ 6 }; // not constexpr

    std::cout << greater(x, y) << " is greater!\n"; // will be evaluated at runtime

    return 0;
}

W tym przykładzie, ponieważ argumenty x i y nie są wyrażeniami stałymi, funkcja nie może zostać rozpoznana w czasie kompilacji. Jednak funkcja nadal będzie rozpoznawana w czasie wykonywania, zwracając oczekiwaną wartość jako wartość inną niż constexpr int.

Kluczowa informacja

Kiedy funkcja constexpr wykonuje ocenę w czasie wykonywania, wykonuje ją tak samo, jak zwykła funkcja (inna niż constexpr). Innymi słowy, constexpr nie ma żadnego efektu w tym przypadku.

Kluczowa informacja

Pozwolenie na ocenę funkcji z typem zwracanym constexpr w czasie kompilacji lub w czasie wykonywania było dozwolone, dzięki czemu pojedyncza funkcja może obsłużyć oba przypadki.

W przeciwnym razie musielibyśmy mieć oddzielne funkcje (funkcję z typem zwracanym constexpr i funkcję z typem zwracanym innym niż constexpr). Wymagałoby to nie tylko zduplikowanego kodu, ale te dwie funkcje musiałyby mieć także różne nazwy!

Przypomnij mi jeszcze raz, dlaczego zależy nam na tym, aby nasze funkcje wykonywały się w czasie kompilacji?

Teraz byłby świetny moment, aby przejrzeć korzyści, jakie mogą zapewnić techniki programowania w czasie kompilacji: 5.5 -- Wyrażenia stałe.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
316 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze