Getting started with arithmetic concepts

By , last updated June 21, 2019

Not long ago, I implemented a Matrix class with template expressions. Sadly, I cannot release it to the public because it belongs to my employer. Such libraries dealing with arithmetic will benefit greatly from concepts. The compile errors from not implemented operators are horrendous.

In this chapter we’ll implement some arithmetic concepts, Addable and Subtractable. If anything, the type represented by a Matrix should be able to support addition and subtraction, but not always. It must be possible to store any type in a Matrix like grid, without doing any arithmetic on it. Then it doesn’t matter if the type stored supports addition and/or subtraction. The best place to put these concepts, is on the matrix operator + and operator -.

Before getting there, the concepts must be defined.

We also need to think through what we really want. A first take on a generic add() method may have some design flaws, assuming the type must be identical at both sides.

template<typename T>
T add(T a, T b)
{
    return a + b;
}

Another assumption is that T + T -> T. That is not always the case. Some date-time libraries returns a timespan when subtracting two points in time.

We need two types. What about the return type? The return type is really the result of a + b, not the type of either a or b.

As luck would have it, in C++11 the keyword decltype was introduced. It is designed exactly for situations where the type might not be inherently clear.

A more modern add method will look like:

template<typename T, typename U>
auto add(T t, U u) -> decltype(t+u)
{
    return t + u;
}

Having those constraints in mind, we can look at creating concepts suited for the modern add method. Coincidently, using a trailing return type with decltype(t+u) acts like a poor mans concept, as it will complain about not being able to deduce the type. With GCC, the error message is surprisingly clear and to the point. The same can’t be said with Visual Studio at the time of writing. Maybe they’ll get better with time.

The Addable concept

The Addable concept should accept two template parameters, and it should perform the intended operation on those two objects in a requires-clause. Next up is a function concept where Addable have been implemented.

template<typename T, typename U>
concept bool Addable()
{
    return requires(T t, U u) { t + u; };
};

This addable function concept can be used to define a method taking only types which satisfy the constraints given by Addable. Extending the add-method above, we only really need to add the requires clause. There are two template parameters of different types, there is automatic return type deduction happening at compile time, and only types who support t + u are accepted into this method.

template<typename T, typename U>
decltype(auto) add(T t, U u) -> requires Addable<T,U>() 
{
    return t + u;
}

This code will compile and build just fine.

int a = 1;
int b = 2;

std::cout << "add(): " << add(a, b) << std::endl;

This code will not build.

// Fail
struct Foo {};
Foo f, g;
add(f,g);   

It gives a brief and to the point error message.

error: cannot call function 'decltype(auto) add(T, U) requires (Addable<T, U>)() [with T = Foo; U = Foo]'
  add(f,g);
         ^
note:   constraints not satisfied
 decltype(auto) add(T t, U u) requires Addable<T,U>()
                ^~~
note: within 'template<class T, class U> concept bool Addable() [with T = Foo; U = Foo]'
 concept bool Addable()
              ^~~~~~~
note:     with 'Foo t'
note:     with 'Foo u'
note: the required expression '(t + u)' would be ill-formed

Subtractable concept

Just as the previous Addable concept, we create a concept that checks if the subtraction operator is present.

template<typename T, typename U>
concept bool Subtractable()
{
    return requires(T t, U u) { t - u; };
};

template<typename T, typename U>
decltype(auto) subtract(T t, U u) requires Subtractable<T, U>()
{
    return t - u;
}
int a = 1;
int b = 2;

std::cout << "subtract(): " << subtract(a, b) << std::endl;

Just as the addable concept, this will build and compile. If you give method a type, which does not implement operator -, then there will be a similar error message as with the Addable concept.

Multiple and Divisible concepts

The Multiple and Divisible concepts aren’t that much different from the previous ones.

template<typename T, typename U>
concept bool Multiple()
{
    return requires(T t, U u) { t * u; };
}

template<typename T, typename U>
concept bool Divisible()
{
    return requires(T t, U u) { t / u; };
}

They may be used like the following multiply() and divisible() methods.

template<typename T, typename U>
decltype(auto) multiply(T t, U u) requires Multiple<T, U>()
{
    return t * u;
}

template<typename T, typename U>
decltype(auto) divide(T t, U u) requires Divisible<T, U>()
{
    return t / u;
}

Using arithmetic concepts may be somewhat contrived. But hang on, there is a grand scheme. Those concepts can be joined together to create a concept, which requires all four mathematical operators to be defined, namely the Arithmetic concept.

Arithmetic concept

Having multiple concepts is fine, but sometimes you have to join multiple concepts. For mathematics, requiring the type to support all mathematical operators is a just requirement.

It’s called a conjunction when multiple concepts are joined together.

Here are the 4 mathematical operators defined with as function concepts.

Using the previous defined concepts for addition, subtraction, division and multiplication, each one of the above concepts are usable in itself, while they also may be used as building blocks in aggregate constraints.

The concept is built by joining the above concepts in a logical chain.

template<typename T, typename U>
concept bool Arithmetic()
{
    return  
        Addable<T, U>() && 
        Subtractable<T, U>() && 
        Multiple<T, U>() && 
        Divisible<T, U>();
}

Using the Arithmetic concept will require all of the concepts to be satisfied before the compiler will allow usage.

A method implementing the Arithmetic concept may be implemented like this:

template<typename T, typename U>
decltype(auto) arithmetic(T t, U u) requires Arithmetic<T, U>()
{
    return (t * u + t - u) / (t + u);
}

GCC will complain about all concepts who are not satisfied if you try and use it on a non satisfactory type, like struct foo{};.

In function 'int main()':
error: cannot call function 'decltype(auto) arithmetic(T, U) requires (Arithmetic<T, U>)() [with T = Foo; U = Foo]'
  arithmetic(f,g);
                ^
note:   constraints not satisfied
 decltype(auto) arithmetic(T t, U u) requires Arithmetic<T, U>()
                ^~~~~~~~~~
note: within 'template<class T, class U> concept bool Arithmetic() [with T = Foo; U = Foo]'
 concept bool Arithmetic()
              ^~~~~~~~~~
note: within 'template<class T, class U> concept bool Addable() [with T = Foo; U = Foo]'
 concept bool Addable()
              ^~~~~~~
note:     with 'Foo t'
note:     with 'Foo u'
note: the required expression '(t + u)' would be ill-formed
note: within 'template<class T, class U> concept bool Subtractable() [with T = Foo; U = Foo]'
 concept bool Subtractable()
              ^~~~~~~~~~~~
note:     with 'Foo t'
note:     with 'Foo u'
note: the required expression '(t - u)' would be ill-formed
note: within 'template<class T, class U> concept bool Multiple() [with T = Foo; U = Foo]'
 concept bool Multiple()
              ^~~~~~~~
note:     with 'Foo t'
note:     with 'Foo u'
note: the required expression '(t * u)' would be ill-formed
note: within 'template<class T, class U> concept bool Divisible() [with T = Foo; U = Foo]'
 concept bool Divisible()
              ^~~~~~~~~
note:     with 'Foo t'
note:     with 'Foo u'
note: the required expression '(t / u)' would be ill-formed

What you can use concepts for is only limited by your imagination.

Professional Software Developer, doing mostly C++. Connect with Kent on Twitter.