65.9K
CodeProject 正在变化。 阅读更多。
Home

Concept Requires C++ 20

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2020年10月18日

CPOL

14分钟阅读

viewsIcon

17470

downloadIcon

115

C++ 20 概念简介。

引言

我一直在想写一篇关于概念(concepts)和requires子句的文章,当我开始为Harlinn.Windows库编写图形API时,Point、Size和Rectangle的各种表示形式提供了一个很好的机会来演示使用概念和requires子句来提高代码可重用性是多么容易。

Windows API有几种用途相似的类型:Point用POINTD2D_POINT_2FD2D_POINT_2U表示;Size用SIZED2D1_SIZE_FD2D1_SIZE_U表示;Rectangle用RECTD2D1_RECT_FD2D1_RECT_U表示。

对于旧版本的库,我已经实现了与POINTSIZERECT二进制兼容的PointSizeRectangle类,但是对D2D_POINT_2FD2D_POINT_2UD2D1_RECT_FD2D1_RECT_U采用相同的方法会导致大量重复代码,以及额外的工作,特别是如果我想在类之间实现合理的互操作性。

正如我们都知道的,这行不通

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

这相当令人厌烦

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

我认为如果我们有一组模板,可以针对各种表示Point、Size和Rectangle的Windows API类型进行特化,使我们能够编写

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 );

我想要一个模板类,可以用于POINTD2D_POINT_2FD2D_POINT_2U。然后,这个模板将分别针对这三种Point类型特化为PointPointFPointU。类似地,我还希望有用于Size类型和Rectangle类型的模板类。最终结果是三个模板类PointTSizeTRectangleT,它们可以很好地协同工作,并与它们特化所针对的Windows API类型协同工作。

为了能够支持我所认为的有意义的各种操作,PointT模板需要能够适应其他似乎提供了与实现相关的信息的类型,而C++ 20使得这比以前容易得多。

Requires

在C++ 20之前,std::enable_if<>是用于排除模板中函数实现的优先机制。我认为大多数人认为这种构造有点碍眼——最好将其视为一种临时解决方案,因为我们都知道概念即将到来。概念将提供一个永久性的解决方案,并且共识是std::enable_if<>将一直可用,直到那时,并且可以在可预见的未来得到支持。然后,在2009年,概念从C++ 11提案中被移除,C++ 14和C++ 17也过去了,而概念依然没有到来。现在,我们终于在C++ 20中有了精简版的概念。

概念为C++语言引入了新的语法元素,而requires子句是其中之一。requires子句可以用作模板中std::enable_if<>的替代品,所以这是一个很好的起点。

std::enable_if<>相比,requires子句有两个优点:

  1. 代码变得**更加**易读。
  2. 求值规则定义得更好。

std::enable_if<>requires子句都是允许我们控制代码是否应从即时编译上下文中排除的机制。std::enable_if<>依赖于替换失败不是错误(<a>SFINAE</a>)来实现这一点,而requires表达式提供了一种简洁的方式来表达对模板参数的要求。要求是可以通过名称查找或检查类型属性和表达式有效性来检查的东西。

无约束模板函数的问题

下面是一个合理的,尽管简单的,Size类的2D图形实现

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; }
};

这个实现不知道SIZED2D1_SIZE_UD2D1_SIZE_FSIZED2D1_POINT_2UD2D1_POINT_2FPOINT或其他任何可以提供与实现相关的信息的结构或类。我们当然可以通过为我们想要支持的每种类型重载构造函数和赋值运算符来添加支持,这会显著增加实现的大小。

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;
}

我在这里肯定是在重复自己,我应该能够通过模板来处理这个问题。D2D1_SIZE_UD2D1_SIZE_F都有widthheight数据成员,而D2D1_POINT_2UD2D1_POINT_2FPOINT都有xy数据成员。

为处理D2D1_SIZE_UD2D1_SIZE_F的构造函数和赋值运算符创建模板实现很简单

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;
}

为处理D2D1_POINT_2UD2D1_POINT_2FPOINT的构造函数和赋值运算符实现模板也很简单

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;
}

上面的模板是无约束的,如果我将它们放入同一个类实现中,编译器会将第二组重载视为无效代码。

本文将解释概念和requires子句如何允许相互竞争的模板重载存在于同一个类中。

PointT模板,第一部分

第一部分介绍了一种仅基于requires子句和requires表达式的方法,来说明PointT模板是如何开发的。第二部分在此基础上进一步演示了用于实现模板最终版本的技术。

PointT模板需要能够适应多种Point表示,这里我们将看看requires子句如何用作std::enable_if<>的替代品。

PointT模板的第一个版本接受两个模板参数,用于定义两个类型

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

其中value_type是用于存储坐标值的数值类型

protected:
    value_type x_;
    value_type y_;
public:

PointType是模板特化所针对的Windows API Point类型。

默认构造函数执行通常预期的操作

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

接下来,我们有一个允许我们指定单独的xy值的构造函数

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 ) )
{
}

在这里,第一次使用requires关键字标记了一个requires子句的开始,该子句限制了此构造函数重载包含在当前编译上下文中的范围,而第二次使用requires开始了requires表达式的定义。这个requires表达式可以有局部参数,局部参数引入的每个名称从其声明点到需求主体的结束大括号都在作用域内。这些参数不能有默认参数,也没有链接、存储或生命周期;它们仅作为用于定义需求的符号。

需求主体由一系列需求组成

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

需求可以引用局部参数(在本例中是uv)、模板参数以及当前编译上下文中可见的任何其他声明。

当替换和约束检查成功时,requires表达式求值为true;当requires表达式的模板参数替换导致类型无效,或者其需求中的表达式无效,或者违反了这些需求的约束时,requires表达式将求值为false;这不会导致程序格式错误,但会从重载集中移除受约束的重载。

当一个不属于模板的requires表达式在其需求中包含无效类型或表达式时,程序将格式错误。

Windows API SIZE结构有两个数据成员,cxcy,此重载将可用于任何具有可访问的cxcy数据成员且可以静态转换为value_typestructclass

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 ) )
{
}

这比编写模板元函数来检测每个成员变量要容易得多

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;
}

然后可以这样使用

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 ) )
{
}

而上面的代码只检查cxcy的存在,我们仍然不知道它是否可以静态转换为value_type

D2D1_SIZE_FD2D1_SIZE_U结构都有两个数据成员,widthheight,此重载将选择任何具有可访问的widthheight数据成员且可以静态转换为value_typestructclass

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 ) )
{
}

每个需求都检查预期操作的语法对编译器是否有意义,丢弃任何requires表达式对模板参数类型没有语法意义的重载。

如果您以前阅读过requires子句,您可能见过这样的代码

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 ) )
{
}

上面的代码肯定可以工作,但是将需求表示为

    { static_cast<value_type>( u.width ) }

代替

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

更简洁,因为这是在模板实现中必须成功的实际操作。如果编译器只在编译模板函数体时才发现代码对模板参数无效,那么代码将无法编译——这将破坏requires子句的目的。在编写需求时,应尽可能简洁。我并不是说您需要表达所有可能的限制,但您选择指定的限制应该是简洁的。

POINTD2D_POINT_2FD2D_POINT_2U结构各有两个数据成员xy,此重载将选择任何具有可访问的xy数据成员且可以静态转换为value_typestructclass

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 ) )
{
}

这意味着类似这样的东西

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

不能这样与PointT模板一起使用,因为MyRect满足了我们对多个构造函数重载施加的约束。但是,它可以这样使用

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

下一个重载可以与PointPointFPointURectangleRectangleFRectangleU以及任何其他公开返回可以静态转换为value_type的类型X()Y()成员函数的类一起使用。

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( ) ) )
{
}

上面的重载将选择

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

最后的构造函数重载将用于SizeSizeFSizeU以及任何其他公开返回可以静态转换为value_typeWidth()Height()成员函数的类,除了任何实现Left()Top()Right()Bottom()的类,如RectangleRectangleFRectangleU

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( ) ) )
{
}

我使用一个概念来消除任何看起来像Rectangle类的东西,首先定义一个用于任何看起来像Rectangle的概念,然后对其进行否定。

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 );
}

上面的方法感觉比基于更传统的元模板编程更好。

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;
}

这种最后的方法也有些脆弱,因为它将无法检测到任何从RectangleT派生的类。

鸭子类型

用于选择可用重载的需求用法应该对很多人来说很熟悉

“如果它走起路来像鸭子,叫起来像鸭子,那它一定是一只鸭子”

到目前为止,我们所看到的每个需求都检查模板参数类型上函数或数据成员的可用性,或者它们检查预期操作在类型上是否有效。特定重载的适用性由成员函数或数据成员的存在,或操作的有效性决定;而不是由类型本身决定。

如果它看起来像一个SIZE结构,那么它就被当作一个SIZE结构来处理;如果它看起来像一个Rectangle,那么它就被当作一个Rectangle来处理。

在C++ 20之前,这种编程风格当然是可能的,但需要大量的精力,这往往使代码过于复杂且难以阅读。概念不仅使得这种编程风格成为可能;它极大地推广了它。

PointT模板,第二部分

到目前为止,我们已经看到,仅requires子句本身就相当强大,它使我们能够轻松地执行以前需要程序员付出巨大努力才能完成的任务。

我也很抱歉地说,我一次又一次地重复同一个要求,当我们到达赋值运算符的重载时,这会变得相当明显。

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;
}

整个requires子句与用于约束构造函数重载之一的requires子句是相同的。

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( ) ) )
{
}

我们需要一种机制,允许我们将需求集分解到代码中一个单独的可重用实体,而这正是C++概念定义的目的。将需求表达式移入概念很简单。

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

现在我们可以这样重写构造函数

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( ) ) )
{
}

以及赋值运算符

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;
}

PointT构造函数重载实现的各种约束都可以重写为概念。

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 > );
}

概念不仅现在可以在实现赋值运算符重载时重用,它们现在也可以在实现SizeTRectangleT模板时重用。

约束PointT模板

由于该模板仅用于一组特定类型,因此限制该模板仅接受预期类型作为有效模板参数是有意义的。

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

其中IsAnyOfT的类型与剩余模板参数中的任何一个类型相同时求值为true。它的实现如下。

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

当参数包展开时,它将展开为一系列std::is_same<Type, TypeListElement1>, std::is_same<Type, TypeListElement2>, ..., std::is_same<Type, TypeListElementN>,这些将传递给std::disjunction_v<>

std::disjunction<>期望每个参数类型都有一个static constexpr bool value成员,并返回第一个value求值为true的类型。std::disjunction_v<>基本上对其参数执行****操作。

WindowsPointType概念展示了一种定义概念的不同方法,其用法也不同。

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

上述方法指定PointT只能为满足WindowsPointType概念约束的类型进行特化。这限制了可能的模板参数为POINTPOINTLD2D_POINT_2FD2D_POINT_2U

PointTSizeTRectangleT模板类在HWCommon.h中实现,并在其中用于定义

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;
}

static_asserts验证转换可以在两个方向上执行。

概念的目的是应用约束来强制执行模板使用的正确性,并且这些特性的设计旨在支持用户轻松和渐进式地采用。约束

  • 允许程序员在模板接口中显式声明一组模板参数的要求。
  • 支持基于约束的函数重载和类模板特化。
  • 通过在使用点根据约束检查模板参数来改进诊断。

概念是约束集和模板参数之间的一种命名关联,概念可以是任何可以在编译时求值的布尔表达式。

此定义

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

等同于

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

前者表示法被称为后者的简写表示法。简写表示法类似于C++语言中使用的类型表示法,并且更直观。

requires子句后面跟着一个布尔表达式作为约束,约束在编译时求值。约束对程序的性能没有影响,因为没有为概念及其约束生成代码。

约束可以为类模板、别名模板、类模板成员函数和函数模板实现。定义概念、requires子句和requires表达式的语法非常灵活。这是一个有效概念

template<typename>
concept C = false;

这个概念也是如此

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

而上面的概念可以像constexpr bool一样使用

constexpr bool isPoint = PointType<POINT>;

概念的模板语法类似于模板类的语法

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_{} {}
};

但是你不能这样做

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

因为概念不应具有关联的约束,并且概念也不能嵌套在classstructunion中。我们也无法使用概念来创建类型的多个定义, 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_{} {}
};

是有效的,但您仍然可以使用概念来决定C的类型

template <typename T>
struct C1 { };

template <typename T>
struct C2 { };

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

简写表示法要求用作typename或class的名称引用已定义的概念。除此之外:概念可以在任何可以使用返回boolconstexpr函数的地方使用,所有重要的都是约束。概念可以基于现有概念定义。

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>;

当两个或多个布尔表达式的约束必须同时满足时,我们称之为合取。在这种情况下,T必须是一个支持加法的类型对象,我们现在可以使用简写表示法来定义一个模板,该模板将计算构造函数参数的总和。

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

这将适用于

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

但不适用于

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

因为DateTime对象不支持DataTime对象的加法。

我们已经看过了

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

这相当于写

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>;

如果其组件中至少有一个求值为true,则求值为true的约束称为析取。

多类型约束

通常有必要表达对多种类型的约束,例如

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( ) ) )
{
}

上面的构造函数重载仅在P实现X()Y()函数,并且两个函数都返回可以静态转换为value_type的内容时才有效,而S必须类似地实现可以静态转换为value_typeWidth()Height()函数。下面的构造函数重载对P有相同的要求,但S必须公开可以静态转换为value_typewidthheight数据成员。

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 ) )
{
}

在这两种情况下,所有约束都必须满足,重载才符合包含在有效重载集中的条件。

完,暂时

C++ 20概念是一项巨大的改进,它消除了对std::enable_if<>的繁琐操作的需要。

它确实使创建C++模板类变得更容易,执行的操作以前需要经验丰富的C++语言律师的技能才能实现,即使那样,大多数人也会发现代码很脆弱,容易以很难推理的方式出错。新的C++ requires表达式易于实现且易于理解,使得日常C++开发更加有趣。😊

所以,下次再见:编码愉快!

历史

  • 2020年10月18日 - 初始发布
  • 2020年10月22日 - 小错误修复
© . All rights reserved.