F.2 — Funkcje Constexpr (część 2)

Funkcja Constexpr wywołuje niewymagane wyrażenia stałe

Można się spodziewać, że funkcja constexpr będzie oceniana w czasie kompilacji, jeśli to możliwe, ale niestety tak nie jest.

W lekcji 5.5 -- Wyrażenia stałe zauważyliśmy, że w kontekstach, które nie wymagają wyrażenia stałego, kompilator może zdecydować, czy ma oceniać wyrażenie stałe w czasie kompilacji lub w czasie wykonywania. W związku z tym każde wywołanie funkcji constexpr, które jest częścią niewymaganego wyrażenia stałego, może zostać ocenione zarówno w czasie kompilacji, jak i w czasie wykonywania.

Na przykład:

#include <iostream>

constexpr int getValue(int x)
{
    return x;
}

int main()
{
    int x { getValue(5) }; // mogą oceniać w czasie wykonywania lub kompilacji
    
    return 0;
}

W powyższym przykładzie, ponieważ getValue() jest constexpr, wywołanie getValue(5) jest wyrażeniem stałym. Jednakże, ponieważ zmienna x nie jest constexpr, nie wymaga inicjatora wyrażenia stałego. Mimo że udostępniliśmy inicjator wyrażenia stałego, kompilator może wybrać, czy getValue(5) wykonuje ocenę w czasie wykonywania, czy w czasie kompilacji.

Kluczowa informacja

Ocena funkcji constexpr w czasie kompilacji jest gwarantowana tylko wtedy, gdy wymagane jest wyrażenie stałe.

Diagnostyka funkcji constexpr w wymaganych wyrażeniach stałych

Kompilator jest nie wymagany do określenia, czy funkcja constexpr jest możliwa do oceny w czasie kompilacji, dopóki nie zostanie faktycznie oceniona w czasie kompilacji. Dość łatwo jest napisać funkcję constexpr, która pomyślnie się kompiluje do użytku w czasie wykonywania, ale następnie nie udaje się jej skompilować, gdy jest oceniana w czasie kompilacji.

Głupi przykład:

#include <iostream>

int getValue(int x)
{
    return x;
}

// Ta funkcja może być obliczona w czasie wykonywania
// Gdy funkcja jest oceniana w czasie kompilacji, funkcja zgłosi błąd kompilacji
// ponieważ wywołanie getValue(x) nie może zostać rozwiązane w czasie kompilacji
constexpr int foo(int x)
{
    if (x < 0) return 0; // potrzebne przed przyjęciem P2448R1 w C++23 (patrz uwaga poniżej)
    return getValue(x);  // wywołaj tutaj funkcję inną niż constexpr
}

int main()
{
    int x { foo(5) };           // ok: zastosujmy w czasie wykonywania
    constexpr int y { foo(5) }; // błąd kompilacji: foo(5) nie może ocenić w czasie kompilacji

    return 0;
}

W powyższym przykładzie, gdy foo(5) jest używany jako inicjator zmiennej x, zostanie ona oceniona w czasie wykonywania. Działa to dobrze i zwraca wartość 5.

Jednakże, gdy foo(5), jest używana jako inicjator zmiennej constexpr y, musi zostać oceniona w czasie kompilacji. W tym momencie kompilator ustali, że wywołanie foo(5) nie może zostać ocenione w czasie kompilacji, ponieważ getValue() nie jest funkcją constexpr.

Dlatego pisząc funkcję constexpr, zawsze jawnie sprawdzaj, czy się ona kompiluje, gdy jest oceniana w czasie kompilacji (wywołując ją w kontekście, w którym wymagane jest wyrażenie stałe, na przykład podczas inicjalizacji constexpr zmienna).

Najlepsza praktyka

Wszystkie funkcje constexpr powinny dać się ocenić w czasie kompilacji, ponieważ będą wymagane w kontekstach wymagających stałego wyrażenia.

Zawsze testuj funkcje constexpr w kontekście, który wymaga stałego wyrażenia, ponieważ funkcja constexpr może działać, gdy jest oceniana w czasie wykonywania, ale kończy się niepowodzeniem w czasie kompilacji.

Dla zaawansowanych czytelników

Przed C++23, jeśli nie istnieją żadne wartości argumentów, które by to powodowały pozwalają na ocenę funkcji constexpr w czasie kompilacji, program jest źle sformułowany (nie jest wymagana żadna diagnostyka). Bez linii if (x < 0) return 0 powyższy przykład nie zawierałby zestawu argumentów, które umożliwiałyby ocenę funkcji w czasie kompilacji, co spowodowałoby nieprawidłową formułę programu. Biorąc pod uwagę, że nie jest wymagana żadna diagnostyka, kompilator może tego nie wymuszać.

Wymaganie to zostało odwołane w C++23 (P2448R1).

Parametry funkcji Constexpr/consteval nie są constexpr

Parametry funkcji constexpr nie są domyślnie constexpr ani nie mogą być deklarowane as constexpr.

Kluczowa informacja

Parametr funkcji constexpr oznaczałby, że funkcję można wywołać tylko z argumentem constexpr, ale tak nie jest -- funkcje constexpr można wywoływać z argumentami innymi niż constexpr, gdy funkcja jest oceniana w czasie wykonywania.

Ponieważ takie parametry nie są constexpr, nie można ich używać w wyrażeniach stałych w funkcji.

consteval int goo(int c)    // c jest nie constexpr i nie można go używać w wyrażeniach stałych
{
    return c;
}

constexpr int foo(int b)    // b nie jest constexpr i nie można go używać w wyrażeniach stałych
{
    constexpr int b2 { b }; // błąd kompilacji: zmienna constexpr wymaga wyrażenia stałego inicjator

    return goo(b);          // błąd kompilacji: wywołanie funkcji consteval wymaga argumentu wyrażenia stałego
}

int main()
{
    constexpr int a { 5 };

    std::cout << foo(a); // ok: wyrażenie stałe a może być użyte jako argument funkcji constexpr foo()
    
    return 0;
}

W powyższym przykładzie funkcja parametr b nie jest constexpr (mimo że argument a jest wyrażeniem stałym). Oznacza to, że b nie można użyć wszędzie tam, gdzie wymagane jest wyrażenie stałe, na przykład w inicjatorze zmiennej constexpr (np. b2) lub w wywołaniu funkcji constexpr (goo(b)).

Parametry funkcji constexpr można zadeklarować jako const, w którym to przypadku są one traktowane jako stałe czasu wykonania.

Powiązana treść

Jeśli potrzebujesz parametrów, które są wyrażeniami stałymi, zobacz 11.9 — Parametry szablonu inne niż typ.

Funkcje Constexpr są domyślnie wbudowane

Gdy funkcja constexpr jest oceniana w czasie kompilacji, kompilator musi być w stanie zobaczyć pełną definicję funkcji constexpr przed takimi wywołaniami funkcji (aby mógł sam przeprowadzić ocenę) W tym przypadku deklaracja forward nie wystarczy, nawet jeśli rzeczywista definicja funkcji pojawi się później w tej samej jednostce kompilacji.

Oznacza to, że funkcja constexpr jest wywoływana wielokrotnie). pliki muszą mieć definicję zawarta w każdej jednostce tłumaczenia — co normalnie byłoby naruszeniem reguły jednej definicji. Aby uniknąć takich problemów, funkcje constexpr są domyślnie wbudowane, co czyni je zwolnionymi z reguły jednej definicji.

W rezultacie funkcje constexpr są często definiowane w plikach nagłówkowych, więc można je #włączyć do dowolnego pliku .cpp wymagającego pełnej definicji.

Reguła

Kompilator musi widzieć. pełna definicja funkcji constexpr (lub consteval), a nie tylko deklaracja forward.

Najlepsza praktyka

Funkcje Constexpr/consteval używane w pojedynczym pliku źródłowym (.cpp) powinny być zdefiniowane w pliku źródłowym powyżej, w którym są używane.

Funkcje Constexpr/consteval używane w wielu plikach źródłowych powinny być zdefiniowane w pliku nagłówkowym, aby można je było uwzględnić w każdym pliku źródłowym.

Dla constexpr wywołania funkcji, które są oceniane tylko w czasie wykonywania, deklaracja forward jest wystarczająca, aby zadowolić kompilator. Oznacza to, że możesz użyć deklaracji forward do wywołania funkcji constexpr zdefiniowanej w innej jednostce tłumaczenia, ale tylko wtedy, gdy wywołasz ją w kontekście, który nie wymaga oceny w czasie kompilacji.

Dla zaawansowanych czytelników

Zgodnie z CWG2166 jest to rzeczywisty wymóg deklaracji forward funkcji constexpr, które są oceniane w czasie kompilacji. jest to, że „funkcja constexpr musi zostać zdefiniowana przed najbardziej zewnętrzną oceną, która ostatecznie skutkuje wywołaniem”. Dlatego jest to dozwolone:

#include <iostream>

constexpr int foo(int);

constexpr int goo(int c)
{
	return foo(c);   // zauważ, że foo nie jest zdefiniowane jeszcze
}

constexpr int foo(int b) // OK, ponieważ foo jest nadal zdefiniowane przed jakimkolwiek wywołaniem goo
{
	return b;
}

int main()
{
	 constexpr int a{ goo(5) }; // to jest najbardziej zewnętrzne wywołanie

	return 0;
}

Zamiarem jest tutaj umożliwienie wzajemnie rekurencyjnych funkcji constexpr (gdzie dwie funkcje constexpr wywołują się nawzajem), co w innym przypadku nie byłoby możliwe.

Podsumowanie

Oznaczenie funkcji jako constexpr oznacza, że może być używana w stałej wyrażenie. Nie oznacza to, że „będzie oceniane w czasie kompilacji”.

Wyrażenie stałe (które może zawierać wywołania funkcji constexpr) jest wymagane tylko do oceny w czasie kompilacji w kontekstach, w których wymagane jest wyrażenie stałe.

W kontekstach, które nie wymagają wyrażenia stałego, kompilator może wybrać, czy obliczyć stałe wyrażenie (które może zawierać wywołania funkcji constexpr) w czasie kompilacji czy w czasie wykonywania.

Środowisko wykonawcze. (nie stałe) wyrażenie (które może zawierać wywołania funkcji constexpr lub wywołania funkcji inne niż constexpr) zostanie obliczone w czasie wykonywania.

Kolejny przykład

Przeprowadźmy jeszcze raz badanie, aby sprawdzić, w jaki sposób funkcja constexpr jest wymagana lub prawdopodobnie będzie dalej oceniana:

#include <iostream>

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

int main()
{
    constexpr int g { greater(5, 6) };              // przypadek 1: zawsze powiadomiony w czasie awarii
    std::cout << g << " is greater!\n";

    std::cout << greater(5, 6) << " is greater!\n"; // przypadek 2: może być oceniany w czasie wykonywania lub kompilacji

    int x{ 5 }; // nie constexpr, ale wartość jest znana w czasie kompilacji
    std::cout << greater(x, 6) << " is greater!\n"; // Przypadek 3: dostępny w czasie wykonywania

    std::cin >> x;
    std::cout << greater(x, 6) << " is greater!\n"; // przypadek 4: zawsze zalecany w czasie wykonywania

    return 0;
}

W przypadku 1 wywołujemy greater() w kontekście wymagającym stałego wyrażenia. Zatem greater() musi zostać obliczone w czasie kompilacji.

W przypadku 2 funkcja greater() jest wywoływana w kontekście, który nie wymaga wyrażenia stałego, ponieważ instrukcje wyjściowe muszą zostać wykonane w czasie wykonywania. Jednakże, ponieważ argumenty są wyrażeniami stałymi, funkcja może zostać oceniona w czasie kompilacji, zatem kompilator może wybrać, czy wywołanie greater() zostanie obliczony w czasie kompilacji lub w czasie wykonywania.

W przypadku 3 wywołujemy greater() z jednym argumentem, który nie jest wyrażeniem stałym. Będzie to więc zazwyczaj wykonywane w czasie wykonywania.

Jednakże ten argument ma wartość znaną w czasie kompilacji. Zgodnie z regułą as-if kompilator może zdecydować się na potraktowanie oceny x jako wyrażenia stałego i oszacowanie tego wywołania do greater() w czasie kompilacji. Ale bardziej prawdopodobne, że oceni to w czasie wykonywania.

Powiązana treść

Zasadę as-if omówimy na lekcji 5.5 -- Wyrażenia stałe.

Zauważ, że nawet funkcje inne niż constexpr mogą być oceniane w czasie kompilacji zgodnie z regułą as-if!

W przypadku 4 wartość argumentu x nie może być znana w czasie kompilacji, więc to wywołanie greater() będzie zawsze oceniane w czasie wykonywania.

Kluczowa informacja

Innymi słowy, możemy kategoryzować prawdopodobieństwo, że funkcja zostanie faktycznie oceniona w czasie kompilacji w następujący sposób:

Zawsze (wymagane przez standard):

  • Funkcja Constexpr jest wywoływana tam, gdzie wymagane jest wyrażenie stałe.
  • Funkcja Constexpr jest wywoływana z innej funkcji ocenianej w czasie kompilacji.

Prawdopodobnie (nie ma powodu, aby tego nie robić):

  • Funkcja Constexpr jest wywoływana tam, gdzie nie jest wymagane wyrażenie stałe, wszystkie argumenty są wyrażeniami stałymi.

Możliwie (jeśli jest zoptymalizowana zgodnie z regułą as-if):

  • Funkcja Constexpr jest wywoływana tam, gdzie nie jest wymagane wyrażenie stałe, niektóre argumenty nie są wyrażeniami stałymi, ale ich wartości są znane pod adresem czasie kompilacji.
  • Funkcja inna niż constexpr, którą można ocenić w czasie kompilacji, wszystkie argumenty są wyrażeniami stałymi.

Nigdy (niemożliwe):

  • Funkcja Constexpr jest wywoływana, gdy wyrażenie stałe nie jest wymagane, niektóre argumenty mają wartości nieznane w czasie kompilacji.

Pamiętaj, że ustawienie poziomu optymalizacji kompilatora może mieć wpływ na to, czy decyduje się ocenić funkcję w czasie kompilacji lub w czasie wykonywania. Oznacza to również, że Twój kompilator może dokonywać różnych wyborów w przypadku kompilacji debugowania i wydań (ponieważ kompilacje debugowania zazwyczaj mają wyłączone optymalizacje).

Na przykład zarówno gcc, jak i Clang nie będą oceniać w czasie kompilacji funkcji constexpr wywoływanej tam, gdzie stałe wyrażenie nie jest wymagane, chyba że kompilator otrzyma polecenie optymalizacji kodu (np. używając opcji -O2 kompilatora).

Dla zaawansowanych czytelników

Kompilator może również zdecydować się na wbudowanie wywołanie funkcji lub nawet całkowicie zoptymalizować wywołanie funkcji. Obydwa mogą mieć wpływ na to, kiedy (lub czy) zostanie oceniona treść wywołania funkcji.

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:  
27 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze