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) }; // may evaluate at runtime or compile-time
    
    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;
}

// This function can be evaluated at runtime
// When evaluated at compile-time, the function will produce a compilation error
// because the call to getValue(x) cannot be resolved at compile-time
constexpr int foo(int x)
{
    if (x < 0) return 0; // needed prior to adoption of P2448R1 in C++23 (see note below)
    return getValue(x);  // call to non-constexpr function here
}

int main()
{
    int x { foo(5) };           // okay: will evaluate at runtime
    constexpr int y { foo(5) }; // compile error: foo(5) can't evaluate at compile-time

    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 is not constexpr, and cannot be used in constant expressions
{
    return c;
}

constexpr int foo(int b)    // b is not constexpr, and cannot be used in constant expressions
{
    constexpr int b2 { b }; // compile error: constexpr variable requires constant expression initializer

    return goo(b);          // compile error: consteval function call requires constant expression argument
}

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

    std::cout << foo(a); // okay: constant expression a can be used as argument to constexpr function 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);   // note that foo is not defined yet
}

constexpr int foo(int b) // okay because foo is still defined before any calls to goo
{
	return b;
}

int main()
{
	 constexpr int a{ goo(5) }; // this is the outermost invocation

	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) };              // case 1: always evaluated at compile-time
    std::cout << g << " is greater!\n";

    std::cout << greater(5, 6) << " is greater!\n"; // case 2: may be evaluated at either runtime or compile-time

    int x{ 5 }; // not constexpr but value is known at compile-time
    std::cout << greater(x, 6) << " is greater!\n"; // case 3: likely evaluated at runtime

    std::cin >> x;
    std::cout << greater(x, 6) << " is greater!\n"; // case 4: always evaluated at runtime

    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