Constrain users with concepts (C++20)

Right from the start, before I get some genius telling me that I “plagiarized” stuff, I want to tell you all that yes, there is some info taken from cppreference, which is the bible of the C++ programmer.

Now that we got rid of this thing, let’s see what I mean by the title. By constraining the user, I simply mean that I will make some C++ entity(class, struct, function etc) available for him but he can use that entity only as I want it to be used.

If you are not familiar with templates or concepts, I have an article here that might help you.

The idea of this article came to me when I remembered of an interview question that I found some time ago on the internet. The question was:

How can you make sure a C++ function can be called as e.g. void foo(int, int) but not as any other type like void foo(long, long)?

The answer was pretty simple and self explanatory. You had to use templates:

// implement foo with int, int
void foo(int a, int b)
{
    // ... some code
}

// delete all other signatures with templates
template <class T1, class T2>
void foo(T1 a, T2 b) = delete;

This way, you can make sure that the compiler will not just implicitly convert some other type of data for a and b, to int and use the implemented function.

But why concepts?

As you already saw in the above example, you can do that without using concepts, per se, only simple templates. However, when it comes to more complex types or constraints, then SFINAE might be involved and implementation becomes a nightmare.

Basically, we are going to use concepts as much as possible since these are godsent for the C++ developer. No more ugly SFINAE.

I’m also going to use some abbreviated function templates for the examples since these also “beautify” the code very much.

Why do it?

Well, this is quite a good question. Why I find it useful to do(in some instances) is because it might save a lot of bugs in the future.

Let’s take a dumb example, but which should prove the point. If we have a sum function, that takes 2 integers as parameters and returns integer as result, nothing can stop us to use this sum function with 2 doubles as parameters. The problem is that, the result will not be the expected one.

Maybe we only intended to use the sum function for integers but the compiler doesn’t care. The compiler will convert the doubles to integers.

int sum(int a, int b)
{
    return a + b;
}

double a=1.1, b=1.1;
double result = sum(a, b); // result is 2 -> expected is 2.2

The user will have no idea about this bug. I know it seems like a dumb mistake to make but it happens and it will be a pain to debug later on. You can find this simple example on godbolt.

Another example: Say we have multiple classes that have a function member calculate. The calculate should return an integer and we want to have a function do_calculations that will sum calculations from 2 objects of these types of classes.

class ClassType1
{
public:
    int calculate() { return 10; }
};

class ClassType2
{
public:
    int calculate() { return 20; }
};

template <class T, class U>
int do_calculations(T a, U b)
{
    return a.calculate() + b.calculate();
}

Now, if we use it as it was intended, everything works well.

ClassType1 a;
ClassType2 b;
int result = do_calculations(a, b); // result is 30, as expected

But what if, by mistake, somebody adds another class definition ClassTypeOther which also has this calculate method but with a different signature and tries to use the do_calculations function.

class ClassTypeOther
{
public:
    bool calculate() { return true; }
};

ClassType1 c;
ClassTypeOther d;

int result1 = do_calculations(c, d); // this compiles but the result is not the expected one

The compiler will implicitly convert the bool into integer and it will always be 1.

I think I’ve made my point and you can see why and how constraining can be useful in some contexts. You can find this last example on godbolt.

How to do it?

I will add here some examples on how you can constrain the user with concepts and other types of templates.

Let’s first take the example with the sum function. We can do this with concepts and abbreviated template functions:

int sum(std::same_as<int> auto a, std::same_as<int> auto b)
{
    return a + b;
}

double a=1.1, b=1.1;
double result = sum(a, b); // this does not compile anymore

Example on godbolt.

How about the second one, with the calculate function? Well, this can be done in multiple ways, but the simplest one would be to have a parent struct for all the classes that can be used for the do_calculations function and again, use a concept to constrain the parameters.

// this does not take any memory
struct IsCalculable
{};

class ClassType1 : public IsCalculable
{
public:
    int calculate() { return 10; }
};

class ClassType2 : public IsCalculable
{
public:
    int calculate() { return 20; }
};

class ClassTypeOther
{
public:
    bool calculate() { return true; }
};

Having this, constraining the do_calculations would look like this:

int do_calculations(std::derived_from<IsCalculable> auto a,
    std::derived_from<IsCalculable> auto b)
{
    return a.calculate() + b.calculate();
}

And we can see that, if we try to use ClassTypeOther as argument to the function, it will not compile anymore.

ClassType1 c;
ClassTypeOther d;
int result1 = do_calculations(c, d); // this does not compile anymore

Link to godbolt.

Final thoughts

I hope I’ve convinced you that constraining a user to use your code as you want is worth it and has value.

Also, concepts are the way to go for this. No more ugly SFINAE. I didn’t want to go into detailed examples for SFINAE, but you can check my article related to SFINAE vs Concepts and you will probably see why I’m so hyped for concepts.

You can find a list of provided concepts by the standard here if you want to take a look or, as you already saw above, you can implement your own.

Leave a Reply

%d bloggers like this: