Saturday, March 18, 2017

Concepts!

Let's start from something very easy, you have the simple function:

int sumScale(int a, int b, int c) {
return c*(a + b);
}
view raw SumScaleForInt hosted with ❤ by GitHub
soon you realize you need another one but for float and before you even need the third one you write it like this:

template <class T>
T sumScale(T a, T b, int c) {
return c*(a + b);
}
looks like you are done, everything goes well until, while compiling your huge project, the compiler gives the following error:

sumScale.cpp: In instantiation of ‘T sumScale(T, T, int) [with T = Point]’:
sumScale.cpp:24:26: required from here
sumScale.cpp:13:15: error: no match for ‘operator+’ (operand types are ‘Point’ and ‘Point’)
return c*(a + b);
~~~^~~~
view raw Error1 hosted with ❤ by GitHub
right, you think, the type Point needs to have defined the operator+, after defining it and after some precious minutes you get now another error:

sumScale.cpp: In instantiation of ‘T sumScale(T, T, int) [with T = Point]’:
sumScale.cpp:28:26: required from here
sumScale.cpp:17:11: error: no match for ‘operator*’ (operand types are ‘int’ and ‘Point’)
return c*(a + b);
~^~~~~~~~
view raw Error2 hosted with ❤ by GitHub
and finally this is the last error and fixing it fixes your whole compilation.

Sometime that Point type is defined in an header that makes your entire project to recompile every time you add a missing feature.

Let see how can concepts can save our time and some headache.

Basically that sumScale function has a strong requirement on the type T. It should be a summable type (it has to support the operator+) and it should be scalable (it has to support operator* with an int), we can express these two concepts in the following way:

template <class T>
concept bool SummableScalable() {
return requires(T a, T b, int c) {
{a + b}->T;
{c * a}->T;
};
}
and then use this defined concept rewriting the sumScale function:

SummableScalable sumScale(SummableScalable a, SummableScalable b, int c) {
return c*(a + b);
}
doing so the error would have been a more useful one:

sumScale_concepts.cpp: In function ‘int main()’:
sumScale_concepts.cpp:40:26: error: cannot call function ‘auto sumScale(auto:1, auto:1, int) [with auto:1 = Point]’
auto c = sumScale(a,b,3);
^
sumScale_concepts.cpp:28:18: note: constraints not satisfied
SummableScalable sumScale(SummableScalable a, SummableScalable b, int c) {
^~~~~~~~
sumScale_concepts.cpp:21:14: note: within ‘template<class T> concept bool SummableScalable() [with T = Point]’
concept bool SummableScalable() {
^~~~~~~~~~~~~~~~
sumScale_concepts.cpp:21:14: note: with ‘Point a’
sumScale_concepts.cpp:21:14: note: with ‘Point b’
sumScale_concepts.cpp:21:14: note: with ‘int c’
sumScale_concepts.cpp:21:14: note: the required expression ‘(a + b)’ would be ill-formed
sumScale_concepts.cpp:21:14: note: the required expression ‘(c * a)’ would be ill-formed
view raw Error3 hosted with ❤ by GitHub
wow, within a single iteration the compiler was able to gives us all the information we needed in order to fix the issue. In case you missed it I'll report for convenience the old and the new version of sumScale function.

template <class T>
T sumScale(T a, T b, int c) {
return c*(a + b);
}
SummableScalable sumScale(SummableScalable a, SummableScalable b, int c) {
return c*(a + b);
}
and this is, in my humble opinion, one of the main advantages of concepts: simplify the generic programming taking rid of the cumbersome template syntax.

Let's go back now to our concept:

template <class T>
concept bool SummableScalable() {
return requires(T a, T b, int c) {
{a + b}->T;
{c * a}->T;
};
}
this concept is the refining of a Scalable concept indeed we can define the SummableScalable concept writing first a Summable concept then refining it in the following way:

template <class T>
concept bool Summable() {
return requires(T a, T b) {
{a + b}->T;
};
}
template <class T>
concept bool SummableScalable() {
return Summable<T>() &&
requires(T a, int c) {
{c * a}->T;
};
}
view raw RefiningConcept hosted with ❤ by GitHub
even better we can combine two concepts Summale + Scalable obtaining a third one:

template <class T>
concept bool Summable() {
return requires(T a, T b) {
{a + b}->T;
};
}
template <class T>
concept bool Scalable() {
return requires(T a, int c) {
{c * a}->T;
};
}
template <class T>
concept bool SummableScalable() {
return Summable<T>() &&
Scalable<T>();
}
I believe when we will get the concepts available on our preferred compiler, for instance the concepts didn't make C++17 and today (at my knowledge) the concepts are implemented only in GCC 6 (using the flag -fconcepts), it will change the face of c++ code even more the c++11 specification did.


2 comments:

Unknown said...

Concepts look like a good idea but I feel the implementation is terrible : 2 new keywords and adding an overload to the semantics of '->' operator.

In C# you can use constraints on type parameters to achieve a similar effect. Although you will loose the benefits of duck typing, I think it is better integrated in the language so more widely understandable.

Here is a short example : https://dotnetfiddle.net/IFYLOY

Gaetano said...

Mr. Monkey. The proposal didn't make it in C++17 so you are still in time to make a better proposal.