C++20 Concepts in Visual Studio 2019 16.3 Preview 2

Back in mid-August, Microsoft released the 2nd preview of Visual Studio 2019 16.3. This is the first version of Visual Studio to support concepts from C++20 both in the compiler and the standard library (header <concepts>) without the changes made at the ISO C++ standards meeting in Cologne. These changes are available when you compile with the /std:c++latest switch.

Concepts allow performing compile-time validation of template arguments and function dispatch based on properties of types. Concepts are very useful in libraries where they can be used to impose compile-time checks on the template arguments of functions or types. For instance, a generic algorithm for sorting a container would require the container type to be sortable for the program to even compile.

In this article, I will show an example with a concept that verifies that a type T can be converted to a std::string via a to_string() function, that is either a member of the class or a free function.

In the code below, OverloadsToString is a concept that checks that a free function called to_string exists, and it takes a single argument of type T and returns a std::string. On the other hand, HasToString is a concept that checks that a type T has a method called to_string that takes no arguments and returns a std::string. These two concepts, are composed together using operator || in a new concept called StringConvertible. A concept composed this way is called a disjuction. On the other hand, a composition of two constraints with operator && is called a conjunction.

#include <string>
#include <concepts>

template <typename T>
concept OverloadsToString = requires (T v)
{
	{to_string(v)}->std::string;
};

template <typename T>
concept HasToString = requires (T v)
{
	{v.to_string()}->std::string;
};

template <typename T>
concept StringConvertible = OverloadsToString<T> || 
                            HasToString<T>;

We can use the StringConvertible concepts to perform checks on types. In the following example, an instance of the class foo can be converted to a std::string using the function to_string (for simplicity, this function does not to much except returning the same string for all instances). For the class bar there is no way to convert an instance of it to a std::string. On the other hand, the class foobar has a method called to_string. We can verify that a type is satifsfying the concept or not using static_assert, as shown below:

struct foo {};
struct bar {};
struct foobar
{
	std::string to_string() { return "foobar"; }
};

std::string to_string(foo const& f)
{
	return "foo";
}

static_assert(StringConvertible<foo>);
static_assert(!StringConvertible<bar>);
static_assert(StringConvertible<foobar>);

As mentioned earlier, concepts are useful to express expectations on the template arguments of a function template or a class template. In the following example, serialize is a function template that expects its template argument to be convertible to a std::string. Similarly, Serializable is a class template that expects the template argument T to be convertible to a std::string.

template <typename T> requires StringConvertible<T>
void serialize(T const & value)
{	
}

template <typename T> requires StringConvertible<T>
struct Serializable
{
	T value;
};

int main()
{
	serialize(foo{});
	serialize(bar{});         // error: the associated constraints are not satisfied
	serialize(foobar{});

	Serializable<foo> f;
	Serializable<bar> b;      // error: the associated constraints are not satisfied
	Serializable<foobar> fb;
}

If you compile this code, the lines marked with error (in the comments) will produce the following errors:

concepts_test.cpp(50,2) : error C2672: 'serialize': no matching overloaded function found
concepts_test.cpp(50,17) : error C7602: 'serialize': the associated constraints are not satisfied
concepts_test.cpp(37) : message : see declaration of 'serialize'

concepts_test.cpp(54,18) : error C7602: 'Serializable': the associated constraints are not satisfied
concepts_test.cpp(43) : message : see declaration of 'Serializable'
concepts_test.cpp(54,20) : error C7602: 'Serializable': the associated constraints are not satisfied
concepts_test.cpp(43) : message : see declaration of 'Serializable'
concepts_test.cpp(54) : error C2641: cannot deduce template arguments for 'Serializable'
concepts_test.cpp(54,21) : error C2783: 'Serializable<T> Serializable(void)': could not deduce template argument for 'T'
concepts_test.cpp(43) : message : see declaration of 'Serializable'
concepts_test.cpp(54,20) : error C2512: 'Serializable': no appropriate default constructor available
concepts_test.cpp(43,1) : message : see declaration of 'Serializable'

The syntax used above (template <typename T> requires StringConvertible<T>) to specify the expectations on the template argument is a bit cumbersome. An alternative that is simpler and more intuitive exists:

template <StringConvertible T>
void serialize(T const& value)
{
}

template <StringConvertible T>
struct Serializable
{
	T value;
};

All the code shown here compiles with Visual Studio 2019 16.3 Preview 2.

Of course, this is barely an introduction to concepts. There are many more things you need to learn about concepts, which you can find on thw web. You can learn more about concepts here:

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.