C++20: Useful concepts: Requiring type T to be derived from a base class

How can I create a concept that requires my parameterized types to inherit from a base class?

I am creating a heavily object-oriented class hierarchy and I want the functions related to that class hierarchy to accept only instances of classes derived from the base class of that hierarchy.

So, for example, I have this base class:

class Object
{
public:
    virtual ~Object() = default;

    virtual std::string to_string() const = 0;
};

I create derived classes from such class, for example: class Int.

class Int : public Object
{
    int n;
public:
    Int(int n) : n{n}  {}

    std::string to_string() const override
    {
        return std::to_string(n);
    }
};

And now I want to create a function template “print” that invokes the method “to_string()” of the given parameter and prints it out.

Because of my design, I want my function template to accept only instances of classes derived from my class Object.

In this case, I can create a C++20 concept using the type trait std::is_base_of<Base, Derived>, like this:

#include <type_traits>

template <typename T>
concept ConceptObject = std::is_base_of<Object, T>::value;

In the lines above, I am creating a new C++20 concept called “ConceptObject” that will require my type T fulfill the requirement of T being a derived class from Object.

So, finally, my function template “print” can be expressed in this way:

template <ConceptObject T>
void print(const T& s)
{
    std::cout << s.to_string() << "\n";
}

And it will only compile if the parameter s is an instance of a class derived from Object:

int main()
{
    print(Int{5});
}

Pretty nice!

If you want to read more about C++ concepts, I have this post introducing them: C++20: Concepts, an introduction

C++20: {fmt} and std::format

In this post I will show a very nice open source library that lets the programmers create formatted text in a very simple, type-safe and translatable way: {fmt}

{fmt} is also the implementation of reference for the C++20 standard std::format (the {fmt}‘s author [https://www.zverovich.net/] submitted his paper to the standard committee) but until this moment (June, 2021) only Microsoft Visual Studio implements it. So I will describe the standalone library for now.

These are the main features we can find in {fmt}:

  • We can specify the format and the variables to be formatted, similar to C printf family.
  • The {fmt} functions do not require the programmer to match the type of the format specifier to the actual values the are being formatted.
  • With {fmt}, programmers can optionally specify the order of values to be formatted. This feature is very very useful when internationalizing texts where the order of the parameters is not always the same as in the original language.
  • Some format validations can be done in compile time.
  • It is based on variadic templates but there are lower-level functions on this library that fail over to C varargs.
  • Its performance its way ahead to C++ std::ostream family.

To test {fmt}, I used Godbolt’s Compiler Explorer.

Hello world

This is a very simple “Hello world” program using {fmt}:

#include <fmt/core.h>

int main()
{
    fmt::print("Hello world using {{fmt}}\n");
}

fmt::print() is a helper function that prints formatted text using the standard output (similar to what std::cout does).

As you can see, I did not specify any variables to be formatted BUT I wrote {{fmt}} instead of {fmt} because {{ and }} are, for the {fmt} functions, escape sequences for { and } respectively, which when used as individual characters, contain a format specifier, an order sequence, or simply mean that will be replaced by a parameterized value.

Using simple variable replacements

For example I want to print out two values, I can do something like:

int age = 16;
std::string name = "Ariana";

fmt::print("Hello, my name is {} and I'm {} years old", name, age);

This will print:

Hello, my name is Ariana and I'm 16 years old.

fmt::print() replaced the {} with the respective variable values. Notice also that I used std::string and it worked seamlessly. Using printf(), the programmers should get the const char* representation of the string to be displayed.

If the programmers specify less variables than {}, the compiler (or runtime) returns an error. More on this below.

If the programmers specify more variables than {}, the extra ones are simply ignored by fmt::print() or fmt::format().

Defining arguments order

The programmers can also specify the order the parameters will be replaced:

void show_numbers_name(bool ascending)
{
    std::string_view format_spec = ascending ? "{0}, {1}, {2}\n" : "{2}, {1}, {0}\n";
    
    auto one = "one";
    auto two = "two";
    auto three = "three";

    fmt::print(format_spec, one, two, three);
}

int main()
{
    show_numbers_name(true);
    show_numbers_name(false);
}

As you can see, the {} can contain a number that tells {fmt} the number of the argument that will be used in such position when formatting the output.

Formatting containers

Containers can easily printed out including #include <fmt/ranges.h>

std::vector<int> my_vec = { 10, 20, 30, 40, 50 };
fmt::print("{}\n", my_vec);

Format errors

There are two ways that {fmt} uses to process errors:

If there is an error in the format, a runtime exception is thrown, for example:

int main()
{
    try
    {
        fmt::print("{}, {}, {}\n", 1, 2);
    }
    catch (const fmt::format_error& ex)
    {
        fmt::print("Exception: {}\n", ex.what());
    }
}

In the example above, I say that I have three parameters but only provided two variables, so a fmt::format_error exception is thrown.

But if the format specifier is always constant, we can specify the format correctness in runtime in this way:

#include <fmt/core.h>
#include <fmt/compile.h>

int main()
{
    fmt::print(FMT_COMPILE("{}, {}, {}\n"), 1, 2);
}

FMT_COMPILE is a macro found in <fmt/compile.h> that performs the format validation in compile-time, thus in this case, a compile-time error is produced.

Custom types

To format your custom types, you must to create a template specialization of the class template fmt::formatter and implement the methods parse() and format(), as in this example:

// My custom type
struct person
{
    std::string first_name;
    std::string last_name;
    size_t social_id;
};

// fmt::formatter full template specialization
template <>
struct fmt::formatter<person>
{
    // Parses the format specifier, if needed (in my case, only return an iterator to the context)
    constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }

    // Actual formatting. The second parameter is the format specifier and the next parameters are the actual values from my custom type
    template <typename FormatContext>
    auto format(const person& p, FormatContext& ctx) {
        return format_to(
            ctx.out(), 
            "[{}] {}, {}", p.social_id, p.last_name, p.first_name);
    }
};

int main()
{
    fmt::print("User: {}\n", person { "Juana", "Azurduy", 23423421 });
}

Neat, huh?

Links

{fmt} GitHub page: https://github.com/fmtlib/fmt

{fmt} API reference: https://fmt.dev/latest/api.html

Compiler explorer: https://godbolt.org/

C++20: Concepts, an introduction

I am pretty new doing C++ Concepts, so I will post here the things I will learn while starting to use them.

C++ Concepts are one of these three large features that are shipped with C++20:

  • Concepts
  • Ranges
  • Modules

Basically, C++ Concepts define a set of conditions or constraints that a data type must fulfill in order to be used as a template argument.

For example, I would want to create a function that sums two values and prints the result. In C++17 and older I would code something like this:

template <typename A, typename B>
void sum_and_print(const A& a, const B& b)
{
    std::cout << (a + b) << "\n";
}

And it works properly for types A and B that DO have the operator+ available. If the types I am using do not have operator+, the compiler naïvely will try to substitute types A and B for the actual types and when trying to use the missing operator on them, it will fail miserably.

The way the compiler works is correct, but failing while doing the actual substitution with no earlier verification is kind of a reactive behavior instead of a proactive one. And in this way, the error messages because of substitution error occurrences are pretty large, hard to read and understand.

C++20 Concepts provide a mechanism to explicit the requirements that, in my example, types A and B would need to implement in order to be allowed to use the “sum_and_print” function template. So when available, the compiler will check that those requirements are fulfilled BEFORE starting the actual substitution.

So, let’s start with the obvious one: I will code a concept that mandates that all types that will honor it will have operator+ implemented. It is defined in this way:

template <typename T, typename U = T>
concept Sumable =
 requires(T a, U b)
 {
    { a + b };
    { b + a };
 };

The new keyword concept is used to define a C++ Concept. It is defined as a template because the concept will be evaluated against the type or types that are used as template arguments here (in my case, T and U).

I named my concept “Sumable” and after the “=” sign, the compiler expects a predicate that needs to be evaluated on compile time. For example, if I would want to create a concept to restrict the types to be only “int” or “double”, I could define it as:

template <typename T>
concept SumableOnlyForIntsAndDoubles = std::is_same<T, int>::value || std::is_same<T. double>::value;

The type trait “std::is_same<T, U>” can be used here to create the constraint.

Back to my first example, I need that operator+ will be implemented in types A and B, so I need to specify a set of requirements for that constraint. The new keyword “requires” is used for that purpose.

So, any definition between braces in the requires block (actually “requires” is always a block, even when only a requirement is specified) is something the types being evaluated must fulfill. In my case, “a+b” and “b+a” must be valid operations. If types T or U do not implement operator+, the requirements will not be fulfilled and thus, the compiler will stop before even trying to substitute A and B for actual types.

So, with such implementation, my function “sum_and_print” works like a charm for ints, doubles, floats and strings!

But, what if I have another type like this one:

struct N
{
    int value;

    N operator+(const N& n) const
    {
        return { value + n.value };
    }
};

Though it implements operator+, it does not implement operator<< needed to work with std::cout.

To add such constraint, I need to add an extra requirement to my concept. So, it could be like this one:

template <typename T, typename U = T>
concept Sumable =
 requires(T a, U b)
 {
    { a + b };
    { b + a };
 }
 && requires(std::ostream& os, const T& a)
 {
     { os << a };
 };

The operator && is used here to specify that those requirements need to be fulfilled: Having operator+ AND being able to do “os << a“.

If my types do not fulfill such requirements, I get an error like this in gcc:

<source>:16:5:   in requirements with 'std::ostream& os', 'const T& a' [with T = N]
<source>:18:11: note: the required expression '(os << a)' is invalid
   18 |      { os << a };
      |        ~~~^~~~

That, though looks complicated, is far easier to read than the messages that the compiler produces when type substitution errors occur.

So, if I want to have my code working properly, I need to add an operator<< overloaded for my type N, having finally something like this:

#include <iostream>

template <typename T, typename U = T>
concept Sumable =
 requires(T a, U b)
 {
    { a + b };
    { b + a };
 }
 && requires(std::ostream& os, const T& a)
 {
     { os << a };
 };

template <Sumable A, Sumable B>
void sum_and_print(const A& a, const B& b)
{
    std::cout << (a + b) << "\n";
}

struct N
{
    int value;

    N operator+(const N& n) const
    {
        return { value + n.value };
    }
};

std::ostream& operator<<(std::ostream& os, const N& n)
{
    os << n.value;
    return os;
}

int main()
{
    sum_and_print( N{6}, N{7});
}

Notice that in my “sum_and_print” function template I am writing “template <Sumable a, Sumable b>” instead of the former “template <typename A, typename B>“. This is the way I ask the compiler to validate such type arguments against the “Sumable” concept.


What if I would want to have several “greeters” implemented in several languages and a function “greet” that will use my greeter to say “hi”. Something like this:

template <Greeter G>
void greet(G greeter)
{
    greeter.say_hi();
}

As you can see, I want my greeters to have a method “say_hi“. Thus, the concept could be defined like this one in order to mandate the type G to have the method say_hi() implemented:

template <typename G>
concept Greeter = requires(G g)
{
    { g.say_hi() } -> std::convertible_to<void>;
};

With such concept in place, my implementation would be like this one:

template <typename G>
concept Greeter = requires(G g)
{
    { g.say_hi() } -> std::convertible_to<void>;
};

struct spanish_greeter
{
    void say_hi() { std::cout << "Hola amigos\n"; }
};

struct english_greeter
{
    void say_hi() { std::cout << "Hello my friends\n"; }
};


template <Greeter G>
void greet(G greeter)
{
    greeter.say_hi();
}


int main()
{
    greet(spanish_greeter{});
    greet(english_greeter{});
}

Why would I want to use concepts instead of, say, base classes? Because:

  1. While using concepts, you do not need to use base classes, inheritance, virtual and pure virtual methods and all that OO stuff only to fulfill a contract on probably unrelated stuff, you simply need to fulfill the requirements the concept defines and that’s it (Interface Segregation of SOLID principles would work nice here, anyway, where your concepts define the minimum needed possible constraints for your types).
  2. Concepts are a “Zero-cost abstraction” because their validation is performed completely at compile-time, and, if properly verified and accepted, the compiler does not generate any code related to this verification, contrary to the runtime overhead needed to run virtual things in an object-oriented approach. This means: Smaller binaries, smaller memory print and better performance!

I tested this stuff using gcc 10.2 and it works like a charm.