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:
POINT pt;
D2D_POINT_2F pt2 = pt;
D2D_POINT_2U pt3 = pt;
And this is rather tiresome:
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:
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:
- Code becomes much more readable.
- 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:
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:
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:
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
:
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:
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:
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:
constexpr PointT( ) noexcept
: x_( 0 ), y_( 0 )
{
}
Next, we have the constructor that allows us to specify separate values for x
and y
:
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:
{ 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
:
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:
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:
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
:
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:
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:
{ static_cast<value_type>( u.width ) }
in place of:
{ 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
:
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:
struct MyRect
{
int x;
int y;
int width;
int height;
};
MyRect myRect{};
Point point2( myRect );
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:
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
:
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:
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
:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
template<WindowsPointType PT>
class PointT
{
...
};
is equivalent to:
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:
template<typename>
concept C = false;
and so is this:
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
:
constexpr bool isPoint = PointType<POINT>;
The template syntax for concept resembles the syntax for a template class:
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:
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:
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:
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
:
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:
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:
template<ObjectWithAdd T>
struct C
{
T value_;
template<ObjectWithAdd... Args>
constexpr C(const Args... args ) noexcept : value_( (args + ...) ) {}
};
This will be valid for:
void foo1()
{
C<int> cint(1,2,3,4,5);
C<TimeSpan> cTimeSpan( TimeSpan(1LL), TimeSpan( 2LL ), TimeSpan( 3LL ) );
}
but not for:
void foo2( )
{
C<DateTime> cDateTime( DateTime( 1LL ), DateTime( 2LL ), DateTime( 3LL ) );
}
since DateTime
objects do not support addition for DataTime
objects.
We have already looked at:
template <typename T>
concept WindowsPointType = Internal::IsAnyOf<T, POINT, POINTL, D2D_POINT_2F, D2D_POINT_2U>;
which is equivalent to writing:
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:
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
.
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