DEV Community

mdh81
mdh81

Posted on • Updated on

C++ Concepts In Action

C++20 has a new feature called Concept that allows developers to specify constraints on template parameters. Template meta programming is a complex topic that stumps even seasoned veterans. I found an interesting usage of this feature to ensure that I can write an overloaded stream insertion operator that can output C++ containers that support range based iteration (basically types that have begin and end methods).

#include <iostream>
#include <unordered_map>
#include <array>
#include <vector>
using namespace std;

template<typename ContainerType>
concept SupportsRangeBasedFor = requires(ContainerType container) {
    container.begin();
    container.end();
};

template<typename KeyType, typename ValueType>
ostream& operator<<(ostream& os, const pair<KeyType, ValueType>& p) {
    os << p.first << "->" << p.second;
    return os;
}

ostream& operator<<(ostream& os, SupportsRangeBasedFor auto&& container) {
    os << "[ ";
    for (const auto& element : container) {
        os << element << ' ';
    }
    os << ']' << endl;
    return os;
}

int main()
{
    cout << vector<int>{1,2,3} << endl;
    cout << array<int,3>{4,5,6} << endl;
    cout << unordered_map<int,int>{{8,9},{10,11},{11,12}} << endl;
    //struct Foo {};
    //cout << Foo {} << endl; // Compiler Error: No overload for operator<< that accepts Foo
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Running this program produces this output:

~/code/cpp/concepts > ./concept
[ 1 2 3 ]

[ 4 5 6 ]

[ 11->12 10->11 8->9 ]

Enter fullscreen mode Exit fullscreen mode

So, we have a single overload that can support any container that has a begin and end method. This is not magic, you could have got this to work without SupportsRangeBasedFor concept, but the magic is what the concept prevents from happening:

Without the constraint on the template parameter, compiler would have happily chosen operator<< overload for Foo and you would get an indecipherable blob of errors caused by the fact that Foo doesn't have begin and end methods. You will see errors of this nature:

concept.cpp:22:30: error: invalid range expression of type 'Foo'; no viable 'begin' function available
    for (const auto& element : container) {
                             ^ ~~~~~~~~~
concept.cpp:35:10: note: in instantiation of function template specialization 'operator<<<Foo>' requested here
    cout << Foo {} << endl; 
Enter fullscreen mode Exit fullscreen mode

Not only that, the compiler would match any ostream.operator<<(...) call to this overload because the template parameter type (introduced by auto type specifier) will match all types.

The type constraint expressed by the concept explicitly forbids the compiler from picking up your template function for types that you don't wish to support. This is a pretty neat feature and I hope to be using this quite widely in my future development to limit the types that my implementation should be chosen for :)

Discussion (0)