14.17 — Constexpr agreguje i zajęcia

W lekcji F.1 -- Funkcje Constexpr, omówiliśmy funkcje constexpr, które są funkcjami, które mogą być oceniane zarówno w czasie kompilacji, jak i w czasie wykonywania. Na przykład:

#include <iostream>

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

int main()
{
    std::cout << greater(5, 6) << '\n'; // greater(5, 6) may be evaluated at compile-time or runtime

    constexpr int g { greater(5, 6) };  // greater(5, 6) must be evaluated at compile-time
    std::cout << g << '\n';             // prints 6

    return 0;
}

W tym przykładzie greater() jest funkcją constexpr i greater(5, 6) jest wyrażeniem stałym, które może być oceniane w czasie kompilacji lub w czasie wykonywania. Ponieważ std::cout << greater(5, 6) wywołania greater(5, 6) w kontekście innym niż constexpr, kompilator może wybrać, czy ma być oceniany greater(5, 6) w czasie kompilacji, czy w czasie wykonywania. Kiedy greater(5, 6) służy do inicjowania zmiennej constexpr g, greater(5, 6) jest wywoływany w kontekście constexpr i musi zostać oceniony w czasie kompilacji.

Rozważmy teraz następujący podobny przykład:

#include <iostream>

struct Pair
{
    int m_x {};
    int m_y {};

    int greater() const
    {
        return (m_x > m_y  ? m_x : m_y);
    }
};

int main()
{
    Pair p { 5, 6 };                  // inputs are constexpr values
    std::cout << p.greater() << '\n'; // p.greater() evaluates at runtime

    constexpr int g { p.greater() };  // compile error: greater() not constexpr
    std::cout << g << '\n';

    return 0;
}

W tej wersji mamy zagregowaną strukturę o nazwie Pair, I greater() jest teraz funkcją składową. Ponieważ jednak funkcja składowa greater() nie jest constexpr, p.greater() nie jest wyrażeniem stałym. Kiedy std::cout << p.greater() wywołania p.greater() (w kontekście innym niż constexpr), p.greater() będą oceniane w czasie wykonywania. Jednakże, gdy próbujemy użyć p.greater() do inicjacji zmiennej constexpr g, pojawia się błąd kompilacji, ponieważ p.greater() nie można ocenić w czasie kompilacji.

Ponieważ dane wejściowe do p są wartościami constexpr (5 i 6), wydaje się, że p.greater() powinno być w stanie oceniane w czasie kompilacji. Ale jak to zrobić?

Funkcje członkowskie Constexpr

Podobnie jak funkcje niebędące składowymi, funkcje składowe można utworzyć constexpr za pomocą constexpr słowa kluczowego. Funkcje składowe Constexpr można oceniać w czasie kompilacji lub w czasie wykonywania.

#include <iostream>

struct Pair
{
    int m_x {};
    int m_y {};

    constexpr int greater() const // can evaluate at either compile-time or runtime
    {
        return (m_x > m_y  ? m_x : m_y);
    }
};

int main()
{
    Pair p { 5, 6 };
    std::cout << p.greater() << '\n'; // okay: p.greater() evaluates at runtime

    constexpr int g { p.greater() };  // compile error: p not constexpr
    std::cout << g << '\n';

    return 0;
}

W tym przykładzie utworzyliśmy greater() funkcję constexpr, więc kompilator może ją ocenić w czasie wykonywania lub kompilacji.

Kiedy wywołujemy p.greater() w wyrażeniu środowiska wykonawczego std::cout << p.greater(), ocenia ona runtime.

Jednakże, gdy p.greater() służy do inicjowania zmiennej constexpr g, pojawia się błąd kompilatora. Chociaż greater() jest teraz constexpr, p w dalszym ciągu nie jest constexpr, zatem p.greater() nie jest wyrażeniem stałym.

Constexpr agreguje

OK, więc jeśli potrzebujemy p być constexpr, zróbmy to constexpr:

#include <iostream>

struct Pair // Pair is an aggregate
{
    int m_x {};
    int m_y {};

    constexpr int greater() const
    {
        return (m_x > m_y  ? m_x : m_y);
    }
};

int main()
{
    constexpr Pair p { 5, 6 };        // now constexpr
    std::cout << p.greater() << '\n'; // p.greater() evaluates at runtime or compile-time

    constexpr int g { p.greater() };  // p.greater() must evaluate at compile-time
    std::cout << g << '\n';

    return 0;
}

Ponieważ Pair jest agregat, a agregaty domyślnie obsługują constexpr, to koniec. To działa! Ponieważ p jest typem constexpr i greater() jest funkcją składową constexpr, p.greater() jest wyrażeniem stałym i może być używane w miejscach, gdzie dozwolone są tylko wyrażenia stałe.

Powiązana treść

Omówiliśmy agregacje w lekcji 13.8 -- Inicjowanie agregatu Struct.

Obiekty klasy Constexpr i constexpr konstruktory

Teraz uczyńmy nasz Pair nieagregatem:

#include <iostream>

class Pair // Pair is no longer an aggregate
{
private:
    int m_x {};
    int m_y {};

public:
    Pair(int x, int y): m_x { x }, m_y { y } {}

    constexpr int greater() const
    {
        return (m_x > m_y  ? m_x : m_y);
    }
};

int main()
{
    constexpr Pair p { 5, 6 };       // compile error: p is not a literal type
    std::cout << p.greater() << '\n';

    constexpr int g { p.greater() };
    std::cout << g << '\n';

    return 0;
}

Ten przykład jest prawie identyczny z poprzednim, z tą różnicą, że Pair nie jest już agregatem (ze względu na posiadanie prywatnych elementów danych i konstruktora).

Kiedy kompilujemy ten program, pojawia się błąd kompilatora dotyczący Pair nie bycia „literałem typ”. Powiedz co?

W C++ instrukcja typ literału to dowolny typ, dla którego możliwe jest utworzenie obiektu w wyrażeniu stałym. Innymi słowy, obiekt nie może być constexpr, chyba że typ kwalifikuje się jako typ dosłowny. A nasz nieagregat Pair nie kwalifikuje się.

Nomenklatura

Typ dosłowny i typ dosłowny to różne (ale powiązane) rzeczy. Literał to wartość constexpr wstawiana do kodu źródłowego. Typ literału to typ, którego można użyć jako typu wartości constexpr. Literał zawsze ma typ dosłowny. Jednakże wartość lub obiekt typu literałowego nie musi być literałem.

Definicja typu literału jest złożona, a podsumowanie można znaleźć na cppreference. Warto jednak zauważyć, że typy literałowe obejmują:

  • Typy skalarne (te przechowujące pojedynczą wartość, takie jak typy podstawowe i wskaźniki)
  • Typy referencyjne
  • Większość agregatów
  • Klasy posiadające konstruktor constexpr

A teraz widzimy, dlaczego nasz Pair nie jest typem dosłownym. Po utworzeniu instancji obiektu klasy kompilator wywoła funkcję konstruktora w celu zainicjowania obiektu. A funkcja konstruktora w naszej klasie Pair nie jest constexpr, więc nie można jej wywołać w czasie kompilacji. Dlatego Pair obiekty nie mogą być constexpr.

Poprawka tego jest prosta: po prostu tworzymy również nasz konstruktor constexpr :

#include <iostream>

class Pair
{
private:
    int m_x {};
    int m_y {};

public:
    constexpr Pair(int x, int y): m_x { x }, m_y { y } {} // now constexpr

    constexpr int greater() const
    {
        return (m_x > m_y  ? m_x : m_y);
    }
};

int main()
{
    constexpr Pair p { 5, 6 };
    std::cout << p.greater() << '\n';

    constexpr int g { p.greater() };
    std::cout << g << '\n';

    return 0;
}

Działa to zgodnie z oczekiwaniami, podobnie jak nasza zagregowana wersja Pair zrobił.

Najlepsza praktyka

Jeśli chcesz, aby Twoja klasa mogła być oceniana w czasie kompilacji, utwórz funkcje składowe i konstruktor constexpr.

Niejawnie zdefiniowane konstruktory są constexpr, jeśli można je jako takie zdefiniować. Konstruktory z domyślnymi ustawieniami muszą być jawnie zdefiniowane jako constexpr.

Wskazówka

Constexpr jest częścią interfejsu klasy i usunięcie go później spowoduje przerwanie funkcji wywołujących wywołujących funkcję w stałym kontekście.

Członkowie Constexpr mogą być potrzebni w przypadku obiektów innych niż constexpr/non-const

W powyższym przykładzie, ponieważ inicjator constexpr zmienna g musi być wyrażeniem stałym, jasne jest, że p.greater() musi być wyrażeniem stałym, a zatem p, instrukcja Pair konstruktorem i greater() muszą wszystkie być constexpr.

Jeśli jednak zastąpimy p.greater() funkcją constexpr, sytuacja się trochę pogorszy oczywiste:

#include <iostream>

class Pair
{
private:
    int m_x {};
    int m_y {};

public:
    constexpr Pair(int x, int y): m_x { x }, m_y { y } {}

    constexpr int greater() const
    {
        return (m_x > m_y  ? m_x : m_y);
    }
};

constexpr int init()
{
    Pair p { 5, 6 };    // requires constructor to be constexpr when evaluated at compile-time
    return p.greater(); // requires greater() to be constexpr when evaluated at compile-time
}

int main()
{
    constexpr int g { init() }; // init() evaluated in compile-time context
    std::cout << g << '\n';

    return 0;
}

Pamiętaj, że funkcja constexpr może być obliczana zarówno w czasie wykonywania, jak i w czasie kompilacji. A kiedy funkcja constexpr ocenia w czasie kompilacji, może wywoływać tylko funkcje zdolne do oceny w czasie kompilacji. W przypadku typu klasy oznacza to funkcje składowe constexpr.

Ponieważ g jest constexpr, init() należy ocenić w czasie kompilacji. W ramach funkcji init() definiujemy p jako non-constexpr/non-const (ponieważ możemy, a nie dlatego, że powinniśmy). Mimo że p nie jest zdefiniowany jako constexpr, p nadal musi zostać utworzony w czasie kompilacji i dlatego wymaga konstruktora constexpr Pair . Podobnie, aby p.greater() było oceniane w czasie kompilacji, greater() musi być funkcją składową constexpr. Jeśli Pair konstruktor lub greater() nie był constexpr, kompilator wystąpiłby błąd.

Kluczowa informacja

Gdy funkcja constexpr wykonuje ocenę w kontekście czasu kompilacji, można wywoływać tylko funkcje constexpr.

Funkcje składowe Constexpr mogą być stałe lub niestałe C++14

W C++11 niestatyczne Funkcje składowe constexpr są domyślnie stałe (z wyjątkiem konstruktorów).

Jednak od wersji C++ 14 funkcje składowe constexpr nie są już domyślnie stałe. Oznacza to, że jeśli chcesz, aby funkcja constexpr była funkcją stałą, musisz jawnie oznaczyć ją jako taką.

Funkcje członkowskie Constexpr inne niż constex mogą zmieniać elementy danych Opcjonalne

Funkcja składowa constexpr niebędąca stałymi może zmieniać elementy danych klasy, o ile obiekt niejawny nie jest stały. Dzieje się tak nawet wtedy, gdy funkcja wykonuje ocenę w czasie kompilacji.

Oto wymyślony przykład:

#include <iostream>

class Pair
{
private:
    int m_x {};
    int m_y {};

public:
    constexpr Pair(int x, int y): m_x { x }, m_y { y } {}

    constexpr int greater() const // constexpr and const
    {
        return (m_x > m_y  ? m_x : m_y);
    }

    constexpr void reset() // constexpr but non-const
    {
        m_x = m_y = 0; // non-const member function can change members
    }

    constexpr const int& getX() const { return m_x; }
};

// This function is constexpr
constexpr Pair zero()
{
    Pair p { 1, 2 }; // p is non-const
    p.reset();       // okay to call non-const member function on non-const object
    return p;
}

int main()
{
    Pair p1 { 3, 4 };
    p1.reset();                     // okay to call non-const member function on non-const object
    std::cout << p1.getX() << '\n'; // prints 0
    
    Pair p2 { zero() };             // zero() will be evaluated at runtime
    p2.reset();                     // okay to call non-const member function on non-const object
    std::cout << p2.getX() << '\n'; // prints 0

    constexpr Pair p3 { zero() };   // zero() will be evaluated at compile-time
//    p3.reset();                   // Compile error: can't call non-const member function on const object
    std::cout << p3.getX() << '\n'; // prints 0

    return 0;
}

Pracując nad tym przykładem, pamiętaj:

  • Funkcja członkowska inna niż stała może modyfikować składowe obiektów innych niż stałe.
  • Funkcję składową constexpr można wywołać zarówno w kontekście wykonawczym, jak i w czasie kompilacji kontekstach.

Te dwie rzeczy działają niezależnie.

W przypadku p1, p1 nie jest stałą. Dlatego możemy wywołać funkcję składową inną niż stała p1.reset() w celu modyfikacji p1. Fakt, że reset() jest constexpr, nie ma tu znaczenia, ponieważ nic, co robimy, nie wymaga oceny w czasie kompilacji.

Klasa p2 Przypadek jest podobny. W tym przypadku inicjatorem p2 jest wywołanie funkcji zero(). Mimo że zero() jest funkcją constexpr, w tym przypadku jest ona wywoływana w kontekście wykonawczym i działa jak normalna funkcja. W zero() tworzymy instancję inną niż stała p, wywołujemy na niej funkcję składową niebędącą stałą p.reset() , a następnie zwracamy p. Zwrócony Pair jest używany jako inicjator dla p2. Fakt, że zero() i reset() są constexpr, nie ma w tym przypadku znaczenia, ponieważ nic, co robimy, nie wymaga oceny w czasie kompilacji.

Klasa p3 przypadek jest interesujący. Ponieważ p3 jest constexpr, musi mieć stały inicjator wyrażenia. Dlatego to wywołanie zero() musi zostać ocenione w czasie kompilacji. A ponieważ oceniamy w kontekście czasu kompilacji, możemy wywoływać tylko funkcje constexpr. Inside zero(), p nie jest stałą (co jest dozwolone, nawet jeśli oceniamy w czasie kompilacji). Ponieważ jednak znajdujemy się w kontekście czasu kompilacji, konstruktor użyty do utworzenia p musi być constexpr. I podobnie jak w przypadku p2 , możemy wywołać funkcję składową niebędącą stałą p.reset() na obiekcie innym niż stała p. Ponieważ jednak znajdujemy się w kontekście czasu kompilacji, reset() funkcja składowa musi mieć wartość constexpr. Funkcja następnie zwraca p, który jest używany do inicjalizacji p3.

Nota autora

Tak, do inicjalizacji obiektu constexpr użyliśmy obiektu innego niż const. Jeśli to psuje ci mózg, prawdopodobnie dzieje się tak dlatego, że nie oddzieliłeś w pełni constexpr od constexpr.

Nie ma wymogu, aby zmienna constexpr była inicjowana wartością constexpr. Może się tak wydawać, ponieważ w większości przypadków inicjujemy zmienną constexpr przy użyciu literałów (które są const) lub innych zmiennych constexpr (które domyślnie są constexpr) i ponieważ terminy const i constexpr mają podobne nazwy.

W rzeczywistości wymaganiem jest, aby zmienna constexpr była inicjowana wyrażeniem stałym. W przypadku funkcji (i operatorów) constexpr nie implikuje stałej, a funkcje constexpr (i operatory) mogą korzystać z obiektów innych niż stałe, a nawet je zwracać.

Ważną rzeczą nie jest stała, ale to, że kompilator może określić wartość obiektu w czasie kompilacji. A w przypadku funkcji constexpr jest to możliwe nawet wtedy, gdy zwracają obiekt inny niż stały!

Funkcje Constexpr, które zwracają referencje (lub wskaźniki) do stałych Opcjonalne

Zwykle nie zobaczysz constexpr i const używanych bezpośrednio obok siebie, ale zdarza się to tylko wtedy, gdy masz funkcję członkowską constexpr, która zwraca odwołanie do stałej (lub (stała)) pointer-to-const).

W naszej Pair klasie powyżej, getX() jest funkcją składową constexpr, która zwraca odwołanie do stałej:

    constexpr const int& getX() const { return m_x; }

To dużo stałych!

Klasa constexpr wskazuje, że funkcja składowa może zostać oceniona w czasie kompilacji. const int& jest typem zwracanym przez funkcję. Skrajna strona const oznacza, że ​​sama funkcja składowa jest const, więc można ją wywołać na obiektach const.

Na marginesie…

Funkcja składowa, która zamiast tego zwróciła wskaźnik const do const, mogłaby wyglądać mniej więcej tak:

constexpr const int* const getXPtr() const { return &m_x; }

Czyż nie jest piękna? NIE? OK, w porządku.

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