Uogólnione stałe wyrażenia - constexpr

C++11 wprowadza dwa znaczenia dla „stałej”:

  • constexpr - stała ewaluowana na etapie kompilacji
  • const - stała, której wartość nie może ulec zmianie

Stałe wyrażenie (constant expression) jest wyrażeniem ewaluowanym przez kompilator na etapie kompilacji. Nie może zawierać wartości, które nie są znane na etapie kompilacji i nie może mieć efektów ubocznych.

Jeśli wyrażenie inicjalizujące dla constexpr nie będzie mogło być wyliczone na etapie kompilacji kompilator zgłosi błąd:

int x1 = 7;  // variable
constexpr int x2 = 7;  // constant at compile-time

constexpr int x3 = x1; // error: initializer is not a contant expression

constexpr auto x4 = x2; // Ok

W wyrażeniu contexpr można użyć:

  • Wartości typów całkowitych, zmiennoprzecinkowych oraz wyliczeniowych
  • Operatorów nie modyfikujących stanu (np. +, ? i [] ale nie = lub ++)
  • Funkcji constexpr
  • Typów literalnych
  • Stałych const zainicjowanych stałym wyrażeniem

Stałe wartości constexpr

W C++11 constexpr przed definicją zmiennej definiuje ją jako stałą, która musi zostać zainicjowana wyrażeniem stałym.

Stała const w odróżnieniu od stałej constexpr nie musi być zainicjowana wyrażeniem stałym.

constexpr int x = 7;

constexpr auto prefix = "Data";

constexpr double pi = 3.1415;

constexpr double pi_2 = pi / 2;
(const double) 1.570750

Funkcje constexpr

W C++11 funkcje mogą zostać zadeklarowane jako constexpr jeśli spełniają dwa wymagania:

  • Ciało funkcji zawiera tylko jedną instrukcję return zwracającą wartość, która nie jest typu void
  • Typ wartości zwracanej oraz typy parametrów powinny być typami dozwolonymi dla wyrażeń constexpr

C++14 znacznie poluzowuje wymagania stawiane przed funkcjami constexpr. Funkcją constexpr może zostać dowolna funkcja o ile:

  • nie jest wirtualna
  • typ wartości zwracanej oraz typy parametrów są typami literalnymi
  • zmienne użyte wewnątrz funkcji są zmiennymi typów literalnych
  • nie zawiera instrukcji asm, goto, etykiet oraz bloków try-catch
  • zmienne użyte wewnątrz funkcji nie są statyczne oraz nie są thread-local
  • zmienne użyte wewnątrz funkcji są zainicjowane

Funkcje ``constexpr`` nie mogą mieć żadnych efektów ubocznych. Zapisywanie stanu do nielokalnych zmiennych jest błędem kompilacji.

Przykład rekurencyjnej funkcji constexpr:

constexpr int factorial(int n)
{
    return (n == 0) ? 1 : n * factorial(n-1);
}

Funkcja constexpr może zostać użyta w kontekście, w którym wymagana jest stała ewaluowana na etapie kompilacji (np. rozmiar tablicy natywnej lub stała będąca parametrem szablonu):

#include <array>

const int size = 2;

int arr1[factorial(1)];
int arr2[factorial(size)];
std::array<int, factorial(3)> arr3;
template <typename T, size_t N>
constexpr size_t size_of_array(T(&)[N])
{
   return N;
}

int arr4[factorial(size_of_array(arr2))];
(int [2]) { 0, 0 }

Instrukcje warunkowe w funkcjach constexpr

Pominięty blok kodu w instrukcji warunkowej nie jest ewaluowany na etapie kompilacji.

constexpr int low = 0;
constexpr int high = 99;
#include <stdexcept>

constexpr int check(int i)
{
    return (low <= i && i < high) ? i : throw std::out_of_range("range error");
}
constexpr int val0 = check(50);  // Ok

constexpr int val2 = check(200);  // Error

Typy literalne

C++11 wprowadza pojęcie typu literalnego (literal type), który może być użyty w stałym wyrażeniu constexpr:

Typem literalnym jest:

  • Typ arytmetyczny (całkowity, zmiennoprzecinkowy, znakowy lub logiczny)
  • Typ referencyjny do typu literalnego (np: int&, double&)
  • Tablica typów literalnych
  • Klasa, która:
    • ma trywialny destruktor (może być default)
    • wszystkie niestatyczne składowe i typy bazowe są typami literalnymi
    • jest agregatem lub ma przynajmniej jeden konstruktor contexpr, który nie jest konstruktorem kopiującym lub przenoszącym (konstruktor musi mieć pustą implementację, ale umożliwia inicjalizację składowych na liście inicjalizującej)
class Complex
{
    double real_, imaginary_;
public:
    constexpr Complex(const double& real, const double& imaginary)
        : real_ {real}, imaginary_ {imaginary}
    {}

    constexpr double real() const { return real_; };
    constexpr double imaginary() const { return imaginary_; }
};
constexpr Complex c1 {1, 2};

Przykłady zastosowań wyrażeń i funkcji stałych (constexpr)

Operacje na polach bitowych

Interesującym zastosowaniem funkcji constexpr jest implementacja operatorów bitowych dla wyliczeń.

namespace Constexpr
{
    enum class Bitmask { b0 = 0x1, b1 = 0x2, b2 = 0x4 };

    constexpr Bitmask operator|(Bitmask left, Bitmask right)
    {
        return Bitmask( static_cast<int>(left) | static_cast<int>(right) );
    }
}

Umożliwia to, zastosowanie czytelnych wyrażeń bitowych np. w etykietach instrukcji switch:

#include <iostream>

using namespace std;
using namespace Constexpr;

Bitmask b = Bitmask::b0 | Bitmask::b1;

switch (b)
{
    case Bitmask::b0 | Bitmask::b1:
        cout << "b0 | b1 - " << static_cast<int>(b) << endl;
        break;
    default:
        cout << "Other value...";
}
b0 | b1 - 3