Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C++

Concept Requires C++ 20

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
18 Oct 2020CPOL14 min read 15.2K   80   6   8
An introduction to C++ 20 concepts
In this article, you will find a step by step introduction to concepts in C++ 20.

Introduction

I have been thinking about writing an article about concepts and the requires clauses for some time, and when I started on the graphics API for the Harlinn.Windows library, the various representations of point, size and rectangle provided a good opportunity to demonstrate how easy it is to use concepts and requires clauses to improve code reusability.

The Windows API has several types serving similar purposes: Points are represented as POINT, D2D_POINT_2F and D2D_POINT_2U; sizes are represented as SIZE, D2D1_SIZE_F and D2D1_SIZE_U; and rectangles are represented as RECT, D2D1_RECT_F and D2D1_RECT_U.

For the old version of the library I had already implemented the Point, Size and Rectangle classes that are binary compatible with POINT, SIZE and RECT respectively, but following the same approach for D2D_POINT_2F, D2D_POINT_2U, D2D1_RECT_F and D2D1_RECT_U would cause a lot of duplicated code, and extra work, especially if I wanted reasonable interoperability between the classes.

This, as we all know, does not work:

C++
POINT pt;
D2D_POINT_2F pt2 = pt;
D2D_POINT_2U pt3 = pt;

And this is rather tiresome:

C++
D2D1_RECT_F layoutRect = D2D1::RectF( clientRect.top * dpiScaleY,
                                        clientRect.left * dpiScaleX,
                                        ( clientRect.right - clientRect.left ) * dpiScaleX,
                                        ( clientRect.bottom - clientRect.top ) * dpiScaleY );

I think it would be quite nice if we had a set of templates that could be specialized for the various Windows API types representing points, sizes and rectangles that allows us to write:

C++
POINT pt{1,1};
SIZE sz{ 4,4 };
RECT rect{ 1,1,5,5 };

Point point1 = pt;
Point point2 = sz;
Point point3 = rect;

PointF pointF1 = pt;
PointF pointF2 = sz;
PointF pointF3 = rect;

SizeF sizeF1 = pt;
SizeF sizeF2 = sz;
SizeF sizeF3 = rect;

Rect rect( point1, sizeF2 );

What I would like is one template class that can be used for POINT, D2D_POINT_2F and D2D_POINT_2U. This template will then be specialized for the three point types as Point, PointF and PointU respectively. Similarly, I would like to have template classes for the size types and the rectangle types. The end result is three template classes PointT, SizeT and RectangleT that work well with each other and the Windows API types they are specialized for.

To be able to support the various operations that I think make sense, the PointT template needs to adapt itself to the other types that appears to provide information relevant for the implementation, and C++ 20 makes this a whole lot easier than before.

Requires

Before C++ 20, std::enable_if<> was the preferred mechanism used to exclude function implementations in templates. I think most felt that this construct was a bit of an eyesore – best viewed as a temporary solution, because we all knew that concepts where coming. Concepts would provide a permanent solution, and the consensus was that std::enable_if<> would do until then, and could be supported for the foreseeable future. Then, in 2009, concepts where pulled from C++ 11 proposal, and C++ 14 and C++ 17 came and went without concepts. Now, we finally have concepts lite in C++ 20.

Concepts introduces new syntactical elements to the C++ language, and the requires clause is one of them. The requires clause can be used as a replacement for std::enable_if<> in templates, so this is a good place to start.

Compared to std::enable_if<> the requires clause has two things going for it:

  1. Code becomes much more readable.
  2. The rules for evaluation are much better defined.

std::enable_if<> and the requires clauses are both mechanisms that allow us to control whether a piece of code should be eliminated from the immediate compilation context or not. std::enable_if<> relies on substitution failure is not an error (<a>SFINAE</a>) to achieve this, while a requires expression provides a concise way to express requirements on template arguments. A requirement is something that can be checked by name lookup or by checking the properties of types and the validity of expressions.

The problem with unconstrained template functions

Here is a reasonable, if simple, implementation of a Size class for 2D graphics:

C++
class Size
{
    int width_;
    int height_;
public:
    constexpr Size( )
        : width_( 0 ), height_( 0 ) { }
    constexpr Size( int width, int height )
        : width_( width ), height_( height ) { }

    constexpr int Width( ) const noexcept
    { return width_; }
    constexpr void SetWidth( int width ) noexcept
    { width_ = width; }
    constexpr int Heigth( ) const noexcept
    { return height_; }
    constexpr void SetHeight( int height ) noexcept
    { height_ = height; }
    constexpr void Assign( int width, int height ) noexcept
    { width_ = width;  height_ = height; }
};

This implementation has no knowledge of SIZE, D2D1_SIZE_U, D2D1_SIZE_F, SIZE, D2D1_POINT_2U, D2D1_POINT_2F, POINT or any other struct or class that can provide information that is relevant for the implementation. We could certainly add support by overloading the constructor and assignment operator for each type we would like to support, adding significantly to the size of the implementation:

C++
constexpr Size( const D2D1_SIZE_U& other ) noexcept
    : width_( static_cast<int>(other.width) ),
      height_( static_cast<int>( other.height) ) { }
constexpr Size( const D2D1_SIZE_F& other ) noexcept
    : width_( static_cast<int>( other.width ) ),
      height_( static_cast<int>( other.height ) ) { }
constexpr Size( const SIZE& other ) noexcept
    : width_( other.cx ),
      height_( other.cy ) { }

constexpr Size( const D2D1_POINT_2U& other ) noexcept
    : width_( static_cast<int>( other.x ) ),
      height_( static_cast<int>( other.y ) ) { }
constexpr Size( const D2D1_POINT_2F& other ) noexcept
    : width_( static_cast<int>( other.x ) ),
      height_( static_cast<int>( other.y ) ) { }
constexpr Size( const POINT& other ) noexcept
    : width_( other.x ),
      height_( other.y ) { }

constexpr Size& operator = ( const D2D1_SIZE_U& other ) noexcept
{
    width_ = static_cast<int>( other.width );
    height_ = static_cast<int>( other.height );
    return *this;
}
constexpr Size& operator = ( const D2D1_SIZE_F& other ) noexcept
{
    width_ = static_cast<int>( other.width );
    height_ = static_cast<int>( other.height );
    return *this;
}
constexpr Size& operator = ( const SIZE& other ) noexcept
{
    width_ = other.cx;
    height_ = other.cy;
    return *this;
}
constexpr Size& operator = ( const D2D1_POINT_2U& other ) noexcept
{
    width_ = static_cast<int>( other.x );
    height_ = static_cast<int>( other.y );
    return *this;
}
constexpr Size& operator = ( const D2D1_POINT_2F& other ) noexcept
{
    width_ = static_cast<int>( other.x );
    height_ = static_cast<int>( other.y );
    return *this;
}
constexpr Size& operator = ( const POINT& other ) noexcept
{
    width_ = other.x;
    height_ = other.y;
    return *this;
}

Here I am certainly repeating myself, and I should be able to handle this using templates. Both D2D1_SIZE_U and D2D1_SIZE_F have width and height data members, and D2D1_POINT_2U, D2D1_POINT_2F and POINT all have x and y data members.

Creating template implementations for the constructor and assignment operator that handles D2D1_SIZE_U and D2D1_SIZE_F is simple:

C++
template<typename T>
constexpr Size( const T& other ) noexcept
    : width_( static_cast<int>(other.width) ),
      height_( static_cast<int>( other.height) ) { }
template<typename T>
constexpr Size& operator = ( const T& other ) noexcept
{
    width_ = static_cast<int>( other.width );
    height_ = static_cast<int>( other.height );
    return *this;
}

So is implementing templates for the constructor and assignment operator that handles D2D1_POINT_2U, D2D1_POINT_2F and POINT:

C++
template<typename T>
constexpr Size( const T& other ) noexcept
    : width_( static_cast<int>( other.x ) ),
      height_( static_cast<int>( other.y ) ) { }
template<typename T>
constexpr Size& operator = ( const T& other ) noexcept
{
    width_ = static_cast<int>( other.x );
    height_ = static_cast<int>( other.y );
    return *this;
}

The templates above are unconstrained, and if I put them into the same class implementation, the compiler will treat the second set of overloads as invalid code.

This article explains how concepts and requires clauses allows competing template overloads to exist within the same class.

The PointT Template, Take 1

Take 1 presents an approach, based solely on requires clauses and requires expression, as to how the PointT template could be developed. Take 2 to builds upon this to demonstrate the techniques used to implement the final version of the template.

The PointT template needs to be able to adapt itself to work with several point representations, and here we will look at how requires clauses can be used as a replacement for std::enable_if<>.

This first edition of the PointT template takes two template arguments used to define two types:

C++
template<typename T, typename PT>
class PointT
{
public:
    using value_type = T;
    using PointType = PT;
protected:
    ...
};

Where value_type is the numeric type used to hold the coordinate values:

C++
protected:
    value_type x_;
    value_type y_;
public:

and PointType is the Windows API point type that the template is specialized for.

The default constructor does what is normally expected:

C++
constexpr PointT( ) noexcept
    : x_( 0 ), y_( 0 )
{
}

Next, we have the constructor that allows us to specify separate values for x and y:

C++
template<typename U, typename V>
    requires requires( U u, V v )
    {
        { static_cast<value_type>( u ) };
        { static_cast<value_type>( v ) };
    }
constexpr PointT( U x, V y ) noexcept
    : x_( static_cast<value_type>( x ) ), y_( static_cast<value_type>( y ) )
{
}

Here, the first use of the requires keyword marks the start of a requires clause, the section constraining the inclusion of this constructor overload into the current compilation context, while the second use of requires starts the definition of a requires expression. This requires expression can have local parameters, and each name introduced by a local parameter is in scope from the point of its declaration until the closing brace of the requirement body. These parameters cannot have default arguments, and have no linkage, storage, or lifetime; they only exist as a notation used to define requirements.

The requirement body is comprised of a sequence of requirements:

C++
{ static_cast<value_type>( u ) };
{ static_cast<value_type>( v ) };

The requirements can refer to local parameters, in this case u and v, template parameters, and any other declarations visible in the current compilation context.

When the substitution and constraint checking succeeds, the requires expression evaluates to true; and when the substitution of template arguments for a requires expression leads to invalid types, or invalid expressions in its requirements, or the violation of the constraints of those requirements, the requires expression evaluates to false; this will not cause the program to be ill-formed, but will remove the constrained overload from the overload set.

A program is ill-formed when a requires expression that is not part of a template contains invalid types or expressions in its requirements.

The Windows API SIZE structure has two data members, cx and cy, and this overload will be available for any struct or class with accessible cx and cy data members that can be statically cast to value_type:

C++
template<typename U>
    requires requires( U u )
    {
        { static_cast<value_type>( u.cx ) };
        { static_cast<value_type>( u.cy ) };
    }
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.cx ) ), y_( static_cast<value_type>( value.cy ) )
{
}

This is so much easier than writing a template meta functions to detect each member variable:

C++
namespace Internal
{
    template <typename T, typename = void>
    struct has_cx : std::false_type {};

    template <typename T>
    struct has_cx<T, decltype( (void)T::cx, void( ) )> : std::true_type {};

    template <typename T, typename = void>
    struct has_cy : std::false_type {};

    template <typename T>
    struct has_cy<T, decltype( (void)T::cy, void( ) )> : std::true_type {};

    template <typename T>
    inline constexpr bool has_cx_and_cy = has_cy<T>::value && has_cx<T>::value;
}

which could then be used like this:

C++
template<typename U, typename = std::enable_if_t< Internal::has_cx_and_cy<U>> >
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.cx ) ), y_( static_cast<value_type>( value.cy ) )
{
}

and the above only checks for the existence of cx and cy, we still do not know whether it can be statically cast to value_type.

Both the D2D1_SIZE_F and the D2D1_SIZE_U structures have two data members, width and height, and this overload will be selected for any struct or class with accessible width and height data members that can be statically cast to value_type:

C++
template<typename U>
    requires requires( U u )
    {
        { static_cast<value_type>( u.width ) };
        { static_cast<value_type>( u.height) };
    }
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.width ) ), 
      y_( static_cast<value_type>( value.height ) )
{
}

Each requirement checks that the syntax of the intended operation makes sense to the compiler, discarding any overload where any of the requires expressions fails to make syntactic sense for the template argument type.

If you have read about the requires clause before, you have probably seen code like this:

C++
template<typename U>
    requires requires( U u )
    {
        { u.width } -> std::convertible_to<value_type>;
        { u.height } -> std::convertible_to<value_type>;
    }
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.width ) ), 
      y_( static_cast<value_type>( value.height ) )
{
}

The above would certainly work, but expressing the requirement as:

C++
{ static_cast<value_type>( u.width ) }

in place of:

C++
{ u.width } -> std::convertible_to<value_type>;

is more concise, as this is the actual operation that must succeed in the implementation of the template. If the compiler only discovers that the code is not valid for the template argument while compiling the body of the template function, then the code will not compile – defeating the purpose of the requires clause. It is good practice to be as concise as possible while writing the requirements. I am not telling you that you need to express every possible requirement, but those that you chose to specify should be succinct.

The POINT, D2D_POINT_2F and D2D_POINT_2U structures each have two data members, x and y, and this overload will be selected for any struct or class with accessible x and y data members that can be statically cast to value_type:

C++
template<typename U>
    requires requires( U u )
    {
        { static_cast<value_type>( u.x ) };
        { static_cast<value_type>( u.y ) };
    }
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.x ) ), 
      y_( static_cast<value_type>( value.y ) )
{
}

This means that something like:

C++
struct MyRect
{
    int x;
    int y;
    int width;
    int height;
};
MyRect myRect{};
Point point2( myRect ); // This line fails to compile

cannot be used like this with the PointT template, because MyRect satisfies the constraints we have put on more than one of the constructor overloads. It can be used like this though:

C++
Point point2( myRect.x, myRect.y );

The next overload can be used with Point, PointF, PointU, Rectangle, RectangleF and RectangleU and any other class that exposes X() and Y() member functions returning a type that can be statically cast to value_type:

C++
template<typename U>
    requires requires( U u )
    {
        { static_cast<value_type>( u.X( ) ) };
        { static_cast<value_type>( u.Y( ) ) };
    }
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.X( ) ) ), y_( static_cast<value_type>( value.Y( ) ) )
{
}

The above overload will be selected for:

C++
class Pt
{
    int cx;
    int cy;
public:
    Pt( ) : cx( 0 ), cy( 0 ) { }
    int X( ) const { return cx; }
    int Y( ) const { return cy; }
};

The final constructor overload will be used for Size, SizeF and SizeU and any other class that exposes Width() and Height() member functions returning a type that can be statically cast to value_type, except anything that implements Left(), Top(), Right() and Bottom() like Rectangle, RectangleF and RectangleU:

C++
template<typename U>
    requires requires( U u )
    {
        { static_cast<value_type>( u.Width() ) };
        { static_cast<value_type>( u.Height() ) };
    } && Internal::NotRectangleClass<U>
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.Width( ) ) ), 
      y_( static_cast<value_type>( value.Height( ) ) )
{
}

I use a concept to eliminate anything that looks like a rectangle class, first defining a concept for anything that look like a rectangle, and then negating that concept:

C++
namespace Internal
{
    template<typename T>
    concept RectangleClass = requires( T t )
    {
        { t.Left( ) } -> std::convertible_to<int>;
        { t.Top( ) } -> std::convertible_to<int>;
        { t.Right( ) } -> std::convertible_to<int>;
        { t.Bottom( ) } -> std::convertible_to<int>;
    };
    template<typename T>
    concept NotRectangleClass = ( RectangleClass<T> == false );
}

The above feels like a better design than one based on more traditional meta template programming:

C++
namespace Internal
{
    template<typename>
    struct IsRectangleT : std::false_type {};

    template<typename T, typename RT, typename PT, typename ST>
    struct IsRectangleT<RectangleT<T, RT,PT,ST>> : std::true_type {};

    template< typename T>
    inline constexpr bool IsRectangle = IsRectangleT<T>::value;

    template< typename T>
    inline constexpr bool IsNotRectangle = IsRectangleT<T>::value == false;
}

This last approach is also a bit brittle, as it will fail to detect any class derived from RectangleT.

Duck Typing

The way requirements are used to select the available overloads should be familiar to many:

“If it walks like a duck and it quacks like a duck, then it must be a duck”

Every requirement, that that we have looked at so far, checks for the availability of functions or data members on the template argument types, or they check if the intended operation will be valid on a type. The suitability of a particular overload is determined by the presence of member functions or data members, or the validity of operations; not by the type itself.

If it looks like a SIZE struct, it is treated like a SIZE struct, and if it looks like a rectangle it is treated as a rectangle.

Before C++ 20, this style of programming was certainly possible, but required a significant effort that tended to make the code overly complicated and hard to read. Concepts not only enables this style of programming; it very much promotes it.

The PointT Template, Take 2

So far, we have seen that the requires clause on its own is quite powerful, and that it enables us to easily perform tasks that used to require a significant effort on the part of the programmer.

I am also, I am sorry to say, repeating the same requirement again and again, and once we get to the overloads for the assignment operator, this gets rather blatant:

C++
template<typename U>
    requires requires( U u )
    {
        { static_cast<value_type>( u.X( ) ) };
        { static_cast<value_type>( u.Y( ) ) };
    }
PointT& operator = ( const U& value ) noexcept
{
    x_ = static_cast<value_type>( value.X( ) );
    y_ = static_cast<value_type>( value.Y( ) );
    return *this;
}

The whole requires clause is identical to one of the requires clauses used to constrain one of the constructor overloads:

C++
template<typename U>
    requires requires( U u )
    {
        { static_cast<value_type>( u.X( ) ) };
        { static_cast<value_type>( u.Y( ) ) };
    }
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.X( ) ) ), y_( static_cast<value_type>( value.Y( ) ) )
{
}

What we need is a mechanism that allows us to factor out the set of requirements into a separate reusable entity in the code, and this is exactly what C++ concept definition is all about. Moving the requirement expressions into a concept is simple:

C++
template<typename T, typename U>
concept ImplementsXAndYFunctions = requires( T t )
{
    static_cast<U>( t.X( ) );
    static_cast<U>( t.Y( ) );
};

and now we can rewrite the constructor as:

C++
template<typename U>
    requires Internal::ImplementsXAndYFunctions<U,value_type>
constexpr PointT( const U& value ) noexcept
    : x_( static_cast<value_type>( value.X( ) ) ), y_( static_cast<value_type>( value.Y( ) ) )
{
}

and the assignment operator as:

C++
template<typename U>
    requires Internal::ImplementsXAndYFunctions<U, value_type>
constexpr PointT& operator = ( const U& value ) noexcept
{
    x_ = static_cast<value_type>( value.X( ) );
    y_ = static_cast<value_type>( value.Y( ) );
    return *this;
}

All the constraints that was implemented using requires clauses for the various PointT constructor overloads can be rewritten as concepts:

C++
namespace Internal
{
    template<typename T, typename U>
    concept StaticCastableTo = requires( T t )
    {
        { static_cast<U>( t ) };
    };

    template<typename T, typename U, typename V>
    concept StaticCastable2To = requires( T t, U u )
    {
        static_cast<V>( t );
        static_cast<V>( u );
    };

    template<typename A, typename B, typename C, typename D, typename V>
    concept StaticCastable4To = requires( A a, B b, C c, D d )
    {
        static_cast<V>( a );
        static_cast<V>( b );
        static_cast<V>( c );
        static_cast<V>( d );
    };

    template<typename T, typename U>
    concept ImplementsXAndYFunctions = requires( T t )
    {
        static_cast<U>( t.X( ) );
        static_cast<U>( t.Y( ) );
    };

    template<typename T, typename U>
    concept ImplementsWidthAndHeightFunctions = requires( T t )
    {
        static_cast<U>( t.Width( ) );
        static_cast<U>( t.Height( ) );
    };

    template<typename T, typename U>
    concept ImplementsLeftTopRightAndBottomFunctions = requires( T t )
    {
        static_cast<U>( t.Left( ) );
        static_cast<U>( t.Top( ) );
        static_cast<U>( t.Right( ) );
        static_cast<U>( t.Bottom( ) );
    };

    template<typename T, typename U>
    concept HasXAndY = requires( T t )
    {
        static_cast<U>( t.x );
        static_cast<U>( t.y );
    };

    static_assert( HasXAndY<POINT, int> );
    static_assert( HasXAndY<D2D_POINT_2F, float> );
    static_assert( HasXAndY<D2D_POINT_2U, UInt32> );

    template<typename T, typename U>
    concept HasCXAndCY = requires( T t )
    {
        static_cast<U>( t.cx );
        static_cast<U>( t.cy );
    };

    static_assert( HasCXAndCY<SIZE, int > );

    template<typename T, typename U>
    concept HasWidthAndHeight = requires( T t )
    {
        static_cast<U>( t.width );
        static_cast<U>( t.height );
    };

    static_assert( HasWidthAndHeight<D2D_SIZE_F, float > );
    static_assert( HasWidthAndHeight<D2D_SIZE_U, UInt32> );

    template<typename T, typename V>
    concept HasLeftTopRightAndBottom = requires( T t )
    {
        { static_cast<V>( t.left ) };
        { static_cast<V>( t.top ) };
        { static_cast<V>( t.right ) };
        { static_cast<V>( t.bottom ) };
    };

    static_assert( HasLeftTopRightAndBottom<RECT, int > );
    static_assert( HasLeftTopRightAndBottom<D2D_RECT_F, float > );
    static_assert( HasLeftTopRightAndBottom<D2D_RECT_U, UInt32 > );
}

Not only can the concepts now be reused when implementing the assignment operator overloads, they can now be reused when implementing the SizeT and RectangleT templates too.

Constraining the PointT template

Since the template is only meant to be used for a specific set of types, it makes sense to constrain the template to only accept the intended types as valid template arguments:

C++
template <typename T>
concept WindowsPointType = Internal::IsAnyOf<T, POINT, POINTL, D2D_POINT_2F, D2D_POINT_2U>;

where IsAnyOf evaluates to true if the type of T is of the same type as any of the remaining template arguments. It is implemented like this:

C++
namespace Internal
{
    template<typename Type, typename... TypeList>
    inline constexpr bool IsAnyOf = std::disjunction_v<std::is_same<Type, TypeList>...>;
}

When the parameter pack is expanded, it will expand into a sequence of std::is_same<Type, TypeListElement1>, std::is_same<Type, TypeListElement2>, …, std::is_same<Type, TypeListElementN> which will be passed to std::disjunction_v<>.

std::disjunction<> expects each of the argument types to have a static constexpr bool value member, and returns the first type for which value evaluates to true. std::disjunction_v<> basically perform an or operation on its arguments.

The WindowsPointType concept demonstrated a different way to define a concept, and the way it is used is different too:

C++
template<WindowsPointType PT>
class PointT
{
    ...
};

The above is a way to specify that PointT can only be specialized for types that satisfies the constraints of the WindowsPointType concept. This limits the possible template arguments to POINT, POINTL, D2D_POINT_2F and D2D_POINT_2U.

The PointT, SizeT and RectangleT template classes are implemented in HWCommon.h, where they are used to define:

C++
using Point = PointT<POINT>;
static_assert( std::is_convertible_v<Point, POINT> );
static_assert( std::is_convertible_v<POINT, Point> );

using Size = SizeT<SIZE>;
static_assert( std::is_convertible_v<Size, SIZE> );
static_assert( std::is_convertible_v<SIZE, Size> );

using Rectangle = RectangleT<RECT>;
static_assert( std::is_convertible_v<Rectangle, RECT> );
static_assert( std::is_convertible_v<RECT, Rectangle> );

using Rect = Rectangle;

namespace Graphics
{
    using PointF = PointT<D2D_POINT_2F>;
    static_assert( std::is_convertible_v<PointF, D2D_POINT_2F> );
    static_assert( std::is_convertible_v<D2D_POINT_2F, PointF> );

    using SizeF = SizeT<D2D1_SIZE_F>;
    static_assert( std::is_convertible_v<SizeF, D2D1_SIZE_F> );
    static_assert( std::is_convertible_v<D2D1_SIZE_F, SizeF> );

    using RectangleF = RectangleT< D2D1_RECT_F>;
    static_assert( std::is_convertible_v<RectangleF, D2D1_RECT_F> );
    static_assert( std::is_convertible_v<D2D1_RECT_F, RectangleF> );

    using RectF = RectangleF;

    using PointU = PointT<D2D_POINT_2U>;
    static_assert( std::is_convertible_v<PointU, D2D_POINT_2U> );
    static_assert( std::is_convertible_v<D2D_POINT_2U, PointU> );

    using SizeU = SizeT<D2D1_SIZE_U>;
    static_assert( std::is_convertible_v<SizeU, D2D1_SIZE_U> );
    static_assert( std::is_convertible_v<D2D1_SIZE_U, SizeU> );

    using RectangleU = RectangleT<D2D1_RECT_U>;
    static_assert( std::is_convertible_v<RectangleU, D2D1_RECT_U> );
    static_assert( std::is_convertible_v<D2D1_RECT_U, RectangleU> );

    using RectU = RectangleU;

    using PointL = Windows::Point;
    using SizeL = Windows::Size;
    using RectangleL = Windows::Rectangle;

    using Point = PointF;
    using Size = SizeF;
    using Rectangle = RectangleF;
    using Rect = Rectangle;
}

The static_asserts verifies that conversions can be performed in both directions.

The intention for concepts is to apply the constraints to enforce the correctness of template use, and the design of these features is intended to support easy and incremental adoption by users. The constraints:

  • allow programmers to explicitly state the requirements of a set of template arguments as part of a template’s interface.
  • support function overloading and class template specialization based on constraints.
  • improve diagnostics by checking template arguments in terms of the constraints at the point of use.

A concept is a named association between a set of constraints and a set of template parameters, and a concept can be any boolean expression that can be evaluated at compile time.

This definition:

C++
template<WindowsPointType PT>
class PointT
{
    ...
};

is equivalent to:

C++
template<typename PT>
    requires WindowsPointType<PT>
class PointT
{
    ...
};

The former notation is called the shorthand notation for the second. The shorthand notation resembles the type notation used throughout the C++ language, and its use is more intuitive.

The requires clause is followed by a boolean expression for the constraints, and the constraints are evaluated at compile time. Constraints have no impact in the performance of a program as no code is generated for a concept and its constraints.

Constraints can be implemented for class templates, alias templates, class template member functions, and function templates. The syntax for defining concepts, requires clauses, and requires expressions is very flexible. This is a valid concept:

C++
template<typename>
concept C = false;

and so is this:

C++
template< typename T>
constexpr bool IsPOINT( )
{
    return std::is_same_v<T, POINT>;
}
template<typename T>
concept PointType = IsPOINT<T>( );

and the above concept can be used like a constexpr bool:

C++
constexpr bool isPoint = PointType<POINT>;

The template syntax for concept resembles the syntax for a template class:

C++
template <typename T>
concept A = std::is_integral_v<T>;

template <typename T>
concept B = std::is_floating_point_v<T>;

template <typename T>
    requires A<T>
struct C
{
    T t_;
    C( T t ) : t_( t ) { }
    C( ) : t_{} {}
};

but you cannot do this:

C++
template <typename T>
    requires (A<T> == false)
concept B = std::is_floating_point_v<T>;

since a concept shall not have associated constraints, and neither can a concept be nested inside a class, struct or union. We cannot use concepts to create multiple definitions of a type, neither:

C++
template <typename T>
    requires A<T>
struct C
{
    T t_;
    C( T t ) : t_( t ) { }
    C( ) : t_{} {}
};

template <typename T>
    requires B<T>
struct C
{
    T t_;
    C( T t ) : t_( t ) {}
    C( ) : t_{} {}
};

nor this:

C++
template <A T>
struct C
{
    T t_;
    C( T t ) : t_( t ) { }
    C( ) : t_{} {}
};

template <B T>
struct C
{
    T t_;
    C( T t ) : t_( t ) {}
    C( ) : t_{} {}
};

is valid, but you can still use a concept to decide the type for C:

C++
template <typename T>
struct C1 { };

template <typename T>
struct C2 { };

template<typename T>
using C = std::conditional_t<A<T>, C1, C2>;

The shorthand notation requires that the name used in place of typename or class refers to a defined concept. Apart from that: a concept can be used anywhere you can use a constexpr function returning a bool, and all that matters are the constraints. A concept can be based on existing concept definitions:

C++
template<typename T>
concept SupportsAdd = requires( T t ) { t + t; };

template <typename T>
concept Object = std::is_object_v<T>;

template<typename T>
concept ObjectWithAdd = SupportsAdd<T> && Object<T>;

When the constraints for two, or more, Boolean expressions, must be satisfied at the same time, we have what is called a conjunction. In this case, T must be an object of a type that supports addition, and we can now use the shorthand notation to define a template that will calculate the sum of the arguments to the constructor:

C++
template<ObjectWithAdd T>
struct C
{
    T value_;
    template<ObjectWithAdd... Args>
    constexpr C(const Args... args ) noexcept : value_( (args + ...) ) {}
};

This will be valid for:

C++
void foo1()
{
    C<int> cint(1,2,3,4,5);
    C<TimeSpan> cTimeSpan( TimeSpan(1LL), TimeSpan( 2LL ), TimeSpan( 3LL ) );
}

but not for:

C++
void foo2( )
{
    // Fails to compile since a DateTime cannot be added to a DateTime
    C<DateTime> cDateTime( DateTime( 1LL ), DateTime( 2LL ), DateTime( 3LL ) );
}

since DateTime objects do not support addition for DataTime objects.

We have already looked at:

C++
template <typename T>
concept WindowsPointType = Internal::IsAnyOf<T, POINT, POINTL, D2D_POINT_2F, D2D_POINT_2U>;

which is equivalent to writing:

C++
template <typename T>
concept WindowsPointType = std::is_same_v<T, POINT> ||
                    std::is_same_v<T, POINTL> ||
                    std::is_same_v<T, D2D_POINT_2F> ||
                    std::is_same_v<T, D2D_POINT_2U>;

A constraint that evaluates to true if at least one of its components evaluates to true is called a disjunction.

Multi-Type Constraints

It is often useful to express constraints on multiple types, for instance:

C++
template<typename P, typename S>
    requires ( Internal::ImplementsXAndYFunctions<P,value_type> &&
        Internal::ImplementsWidthAndHeightFunctions<S, value_type> )
constexpr RectangleT( const P& position, const S& size ) noexcept
    : left_( static_cast<value_type>( position.X( ) ) ),
        top_( static_cast<value_type>( position.Y( ) ) ),
        right_( static_cast<value_type>( position.X( ) ) + 
                            static_cast<value_type>( size.Width( ) ) ),
        bottom_( static_cast<value_type>( position.Y( ) ) + 
                            static_cast<value_type>( size.Height( ) ) )
{
}

The above constructor overload is only valid if P implements the X() and Y() functions, and both functions returns something that can be static_cast to value_type, while S must similarly implement Width() and Height() functions. The constructor overload below has the same requirements for P, but S must expose width and height data members that can be static_cast to value_type.

C++
template<typename P, typename S>
    requires ( Internal::ImplementsXAndYFunctions<P, value_type> &&
        Internal::HasWidthAndHeight<S, value_type> )
constexpr RectangleT( const P& position, const S& size ) noexcept
    : left_( static_cast<value_type>( position.X( ) ) ),
        top_( static_cast<value_type>( position.Y( ) ) ),
        right_( static_cast<value_type>( position.X( ) ) + 
                            static_cast<value_type>( size.width ) ),
        bottom_( static_cast<value_type>( position.Y( ) ) + 
                            static_cast<value_type>( size.height ) )
{
}

In both cases, all the constraints must be satisfied for the overloads to be eligible for inclusion into the valid overload set.

The End, For Now

C++ 20 concepts is a huge improvement that removes the need for fiddling about with std::enable_if<>.

It certainly makes it easier to create C++ template classes, performing operations that earlier would require the skills of a seasoned C++ language lawyer to implement, and even then, most would find the code brittle and easy to break in ways that is very hard to reason about. The new C++ requires expressions is easy to implement and easy to reason about, making day-to-day C++ development a lot more fun. 😊

So, until next time: Happy coding!

History

  • 18th October, 2020 - Initial post
  • 22nd October, 2020 - minor bugfix

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Sea Surveillance AS
Norway Norway
Chief Architect - Sea Surveillance AS.

Specializing in integrated operations and high performance computing solutions.

I’ve been fooling around with computers since the early eighties, I’ve even done work on CP/M and MP/M.

Wrote my first “real” program on a BBC micro model B based on a series in a magazine at that time. It was fun and I got hooked on this thing called programming ...

A few Highlights:

  • High performance application server development
  • Model Driven Architecture and Code generators
  • Real-Time Distributed Solutions
  • C, C++, C#, Java, TSQL, PL/SQL, Delphi, ActionScript, Perl, Rexx
  • Microsoft SQL Server, Oracle RDBMS, IBM DB2, PostGreSQL
  • AMQP, Apache qpid, RabbitMQ, Microsoft Message Queuing, IBM WebSphereMQ, Oracle TuxidoMQ
  • Oracle WebLogic, IBM WebSphere
  • Corba, COM, DCE, WCF
  • AspenTech InfoPlus.21(IP21), OsiSoft PI


More information about what I do for a living can be found at: harlinn.com or LinkedIn

You can contact me at espen@harlinn.no

Comments and Discussions

 
PraiseMy vote of 5 Pin
Sergey Alexandrovich Kryukov20-Oct-20 21:02
mvaSergey Alexandrovich Kryukov20-Oct-20 21:02 
AnswerRe: My vote of 5 Pin
Espen Harlinn21-Oct-20 14:21
professionalEspen Harlinn21-Oct-20 14:21 
GeneralC++ qualities Pin
Sergey Alexandrovich Kryukov21-Oct-20 15:10
mvaSergey Alexandrovich Kryukov21-Oct-20 15:10 
GeneralRe: C++ qualities Pin
Espen Harlinn22-Oct-20 13:20
professionalEspen Harlinn22-Oct-20 13:20 
Questionis it implemented in Visual Studio 2019? Pin
Southmountain19-Oct-20 10:54
Southmountain19-Oct-20 10:54 
when can we use C++20 in Visual Studio?
diligent hands rule....

AnswerRe: is it implemented in Visual Studio 2019? Pin
Espen Harlinn19-Oct-20 12:22
professionalEspen Harlinn19-Oct-20 12:22 
GeneralMy vote of 5 Pin
Member 1052334719-Oct-20 1:50
Member 1052334719-Oct-20 1:50 
GeneralRe: My vote of 5 Pin
Espen Harlinn19-Oct-20 12:23
professionalEspen Harlinn19-Oct-20 12:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.