14.17 — Constexpr aggregates and classes

In lesson 5.8 -- Constexpr and consteval functions, we covered constexpr functions, which are functions that may be evaluated at either compile-time or runtime. For example:

#include <iostream>

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

int main()
{
    std::cout << greater(5, 6);        // 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;                    // prints 6

    return 0;
}

In this example, greater() is a constexpr function, and greater(5, 6) is a constant expression, which may be evaluated at either compile-time or runtime. Because std::cout << greater(5, 6) calls greater(5, 6) in a non-constexpr context, the compiler is free to choose whether to evaluate greater(5, 6) at compile-time or runtime. When greater(5, 6) is used to initialize constexpr variable g, greater(5, 6) is called in a constexpr context, and must be evaluated at compile-time.

Now consider the following similar example:

#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();        // p.greater() evaluates at runtime

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

    return 0;
}

In this version, we have an aggregate struct named Pair, and greater() is now a member function. However, because member function greater() is not constexpr, p.greater() is not a constant expression. When std::cout << p.greater() calls p.greater() (in a non-constexpr context), p.greater() will be evaluated at runtime. However, when we try to use p.greater() to initialize constexpr variable g, we get a compile error, as p.greater() cannot be evaluated at compile-time.

Since the inputs to p are constexpr values (5 and 6), it seems like p.greater() should be capable of being evaluated at compile-time. But how do we do that?

Constexpr member functions

Just like non-member functions, member functions may be made constexpr via use of the constexpr keyword. This tells the compiler that the member function can be evaluated at either compile-time or runtime.

#include <iostream>

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

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

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

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

    return 0;
}

In this example, we’ve made greater() a constexpr function, so the compiler can evaluate it at either runtime or compile-time. When we call p.greater() in runtime expression std::cout << p.greater(), it evaluates at runtime.

However, this program still generates a compiler error when p.greater() is used to initialize constexpr variable g. Although greater() is now constexpr, p.greater() is still not a constant expression, because p is not constexpr.

Constexpr aggregates

Okay, so if we need p to be constexpr, let’s just make it 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();        // p.greater() evaluates at runtime or compile-time

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

    return 0;
}

Since Pair is an aggregate, and aggregates implicitly support being constexpr, we’re done. This works! Since p is a constexpr type, and greater() is a constexpr member function, p.greater() is a constant expression and can be used in places where only constant expressions are allowed.

Related content

We covered aggregates in lesson 13.8 -- Struct aggregate initialization.

Constexpr class objects and constexpr constructors

Now let’s make our Pair a non-aggregate:

#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();

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

    return 0;
}

This example is almost identical to the prior one, except Pair is no longer an aggregate (due to having private data members and a constructor).

When we compile this program, we get a compiler error about Pair not being a “literal type”. Say what?

In C++, a literal type is any type for which it might be possible to create an object within a constant expression. Put another way, an object can’t be constexpr unless the type qualifies as a literal type. And our non-aggregate Pair does not qualify.

Nomenclature

A literal and a literal type are distinct (but related) things. A literal is a constexpr value that is inserted into the source code. A literal type is a type that can be used as the type of a constexpr value. A literal always has a literal type. However, a value or object with a literal type need not be a literal.

The definition of a literal type is complex, and a summary can be found on cppreference. However, it’s worth noting that literal types include:

  • Scalar types (those holding a single value, such as fundamental types and pointers)
  • Reference types
  • Most aggregates
  • Classes that have a constexpr constructor

And now we see why our Pair isn’t a literal type. When a class object is instantiated, the compiler will call the constructor function to initialize the object. And the constructor function in our Pair class is not constexpr, so it can’t be invoked at compile-time. Therefore, Pair objects cannot be constexpr.

The fix for this is simple: we just make our constructor constexpr as well:

#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();

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

    return 0;
}

This works as expected, just like our aggregate version of Pair did.

Best practice

If you want your class to be able to be evaluated at compile-time, make your member functions and constructor constexpr.

Tip

Constexpr is part of the interface of the class, and removing it later will break callers who are calling the function in a constant context.

Constexpr member functions may be const or non-const C++14

In C++11, non-static constexpr member functions are implicitly const (except constructors).

However, as of C++14, constexpr member functions are no longer implicitly const. This means a constexpr member function can change data member of the class, so long as the implicit object isn’t const.

Here’s a contrived example of this:

#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 not const
    {
        m_x = m_y = 0;
    }

    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();       // call constexpr member function on non-const object
    return p;
}

int main()
{
    constexpr Pair p2 { zero() }; // zero() must evaluate at compile-time
    std::cout << p2.getX();       // prints 0

    return 0;
}

Constexpr functions that return const references (or pointers)

Normally you won’t see constexpr and const used right next to each other, but one case where this does happen is when you have a constexpr member function that returns a const reference (or pointer-to-const).

In our Pair class above, getX() is a constexpr member function that returns a const reference:

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

That’s a lot of const-ing!

The constexpr indicates that the member function can be evaluated at compile-time. The const int& is the return type of the function. The rightmost const means the member-function itself is const so it can be called on const objects.

guest
Your email address will not be displayed
Find a mistake? Leave a comment above!
Correction-related comments will be deleted after processing to help reduce clutter. Thanks for helping to make the site better for everyone!
Avatars from https://gravatar.com/ are connected to your provided email address.
Notify me about replies:  
6 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments