DEV Community

Cover image for Using C++ concepts for great good :) part 1
giannicrivello
giannicrivello

Posted on

Using C++ concepts for great good :) part 1

Motivated by Haskell’s type system I have been learning the C++20 feature, concepts. Concepts are to C++ what type classes are to Haskell (conceptually at least). In a nutshell, concepts allow us to declaratively describe a type based off of requirements that the type must satisfy. Often this is paired with C++ type_traits that aid in this description.

Why is this cool? Well, it allows us to constrain polymorphic types elegantly. This leads to better error messages, safer code, and overall tighter C++ (in my opinion). In my short example I am going to go through how to ensure that a template argument derives some notion of printing to the terminal. That is, I want to make sure any template argument passed into my container has an overloaded std::ostream operator <<. This might not be the most compelling example of concepts but I found it nifty and those who aren't experts may also.

Let’s do it!

//this is a container
template <typename T>
struct Container {
    using value_type = T; //type alias
    T val;                //storage

    void show() {
       std::cout << val; 
     }
}
Enter fullscreen mode Exit fullscreen mode

So we start with a simple struct Container with a type alias value_type and a show function that simply prints the val member to the screen. This works as it should for the following substitutions for type T.

int main() {
    Container<std::string> t{"Concepts rule!"}; 
    t.show(); //prints "Concepts rule!" to the terminal
   return 0;
}
Enter fullscreen mode Exit fullscreen mode
int main() {
    Container<int> t{69};
    t.show(); //prints 69 to the terminal
   return 0;
}
Enter fullscreen mode Exit fullscreen mode

But as I am sure you know, this is not fine and dandy for all types passed as template arguments. Consider the example of passing in a user defined type to Container.

template<typename T>
struct Box { /* implementation code ... */ };

int main() {
    Container<Box<int>> t{};  //this is fine
    t.show();                 //compiler error!
   return 0;
Enter fullscreen mode Exit fullscreen mode

This blows up in our face when we substitute a template argument for something that does not define the << operator. Not at the time of template instantiation but because we call t.show().

We get an error message that says,

`no match for 'operator<<' (operand types are 'std::ostream' {aka 'std::basic_ostream<char>'} and 'Bar<int>'
Enter fullscreen mode Exit fullscreen mode

This is obvious, right? The error message clearly says that the compiler has no idea what it means to apply the infix operator << with your user defined type Bar<int> as an argument.

Well, let’s say that we want to add the constraint that any type that does not implement a notion of printing to the terminal is incorrect. In our current example, if we never call show, the compiler error won’t arise! This is bad. We want to ensure that at template instantiation we are not allowed to pass in a type that does not comply to our constraint.

This is different than a generic compiler error based on a language mechanism like operator overloading. We are declaring the “acceptable types” of Container.

This is where concepts come in. Concepts allow us to express ideas related to types such that a template parameter has semantic meaning. We are constraining polymorphic types.

Lets make a concept called Showable

template<typename T>
concept Showable = requires(T& t, std::ostream& os) {
    {os << t} -> std::same_as<std::ostream&>; 
};
Enter fullscreen mode Exit fullscreen mode

Let’s un-pack this. We introduce the concept keyword that creates a new set of types called Showable. The requires syntax used in this example is in the form of requires(parameter list...){requirement sequence...}. The parameters do nothing else other than help us describe requirements in the requirements sequence. The requires clause reads as, given a T& and a std::ostream& the expression {os << t} returns std::ostream&.

Putting the whole thing together should read as, Showable takes a type T such that the operation of os << t where os is of type std::ostream& and t is of type T& is valid and returns a value of std::ostream&.

Our new concept creates a predicate that can be used in place of typename for our templated code. This predicate fails if a type passed into our template does not satisfy the above requirements. Failure in this context is much closer to what we want semantically.

From here we change our Container template struct to

template <Showable T> // change typename to our concept Showable
struct Container {
    using value_type = T;
    T val;

    void show() {
       std::cout << val; 
     }
};
Enter fullscreen mode Exit fullscreen mode
int main() {
    Container<Box<int>> t; // where Box<T> does not implement an overloaded <<
   return 0;
}
Enter fullscreen mode Exit fullscreen mode
error: constraints not satisfied for class template 'Container' [with T = Box<int>]
    Container<Box<int>> t;
    ^~~~~~~~~~~~~~~~~~~

note: because 'Box<int>' does not satisfy 'Showable'
template <Showable T>
Enter fullscreen mode Exit fullscreen mode

Yay! We just wrote a concept! We said something about the type of types allowed as an argument to a user defined type….meta. This was a contrived example but it doesn’t take much convincing to see that this C++ feature is bad ass and super powerful.

Here is the semantically “equivalent” code in Haskell (in respect to constraining polymorphic types)

data Container a =  Container a    -- a simple data constructor

show' :: Show(a) => a -> String    -- redefinition of show using the standard lib show function
show' = show

main = do
    putStrLn $ show' (Container "this isn't c++?!")
Enter fullscreen mode Exit fullscreen mode

Where Show(a) constrains the polymorphic type applied to our show' function. In the above code our type Container does not derive from the type class Show which means that the above code is ill formed and results in the compiler error below

 * No instance for (Show (Container String))
        arising from a use of show
Enter fullscreen mode Exit fullscreen mode

We fix this issue with the following change

data Container a =  Container a deriving(Show) -- our data constructor now derives Show

show' :: Show(a) => a -> String                -- redefinition of show using the standard lib show function
show' = show

main = do
    putStrLn $ show' (Container "this isn't c++?!")
Enter fullscreen mode Exit fullscreen mode

Our constraint is met and now produces the output we were expecting :)

Container "this isn't c++?!"
Enter fullscreen mode Exit fullscreen mode

Image description

Top comments (0)