cppcon2021 - C++20Templates:The next level:Concepts and more

C++20 Templates: The next level - Concepts and more

该演讲内容涵盖了concepts的基础,拖着进度条也不会落下什么东西,基本是面向初学者了。

那我们先看看concepts能干什么。

Concepts

concepts可以将一个类型的需求“公式化”,你在写一个模板时,如果你对你的模板参数有什么需求,你可以使用concepts。一个concepts可包括多个concepts的定义和requirements

template<typename T, typename U>
concept MyConcept = std::same_as<T,U> &&
				(std::is_class_v<T> || std::is_enum_v<T>);

这里的MyConcept是由其他concept——same_as以及requirement——is_class_v&is_enum_v组成的。

拿concept可以拿来干什么呢?

举个例子:

const auto x = Add(2,3,4,5);
const auto y = Add(2,3);
const auto z = Add(2,3.14); //error

我们想实现一个可变参数模板函数,实现任意个同类型量相加,返回他们的和。

既然我们想要模板能够自己检查出不同类型的错误,我们可以实现一个concept,不过在那之前,我们先看看假如用c++17应该怎么实现。

template<typename T,typename... Ts>
constexpr inline bool are_same_v = std::conjunction_v<std::is_same<T, Ts>...>;

template<typename T, typename...>
struct first_arg {
    using type = T;
};
template<typename... Args>
using first_arg_t = first_arg<Args...>::type;
template<typename... Args>
std::enable_if_t<are_same_v<Args...>, first_arg_t<Args...>>
Add(Args&... args){
	return (... + args);
}

are_same_v是检查所有参数类型是否一致,first_arg_v来获得所有参数的第一个参数类型,这里用来获取返回值。

enable_if_v放在返回值位置,如果满足条件(即类型相同),那么返回值类型为第一个参数类型,如果不满足,编译就会报错。

(... + args)是c++的unfold运算,算是语法糖吧。

那到了c++20,实现就很简单了。

template<typename... Args>
requires are_same_v<Args...>
auto Add(Args&... args) noexcept{
	return (... + args);
}

concepts的引入使得模板约束变得比以前整洁太多,可读性也很高,学习难度瀑布式下降。

concepts可使用的位置如下:

// you can use your concepts to replace the "C"

template<C T>     //type-constraint
requires C<T>     //requires-clause
C auto /*<-costrained placeholder type->*/ Fun(C auto param) requeires C<T> //trailing requires-clause

但是Add()函数的要求可远远不止这些。

Requires Expression

requires(T t, U u)
{
	//some requirements
}

requires表达式会尝试编译表达式内的代码,查看这些代码是否合法。整个表达式可以想成返回一个bool,如果代码非法,则requires会false。

我们的add函数的参数ARGS需要以下几个要求——可加(即存在重载的operator+),类型相同,参数包至少含有一个参数,operator必须是noexcept的。

于是:

requires(Args... args)
{
	(... + args);
	requires are_same_v<Args...>;
	requires sizeof...(Args) > 1;
	{(... + args)} noexcept;
	{(... + args)} -> same_as<first_arg_t<Args...>>;
}

可以发现,requires expression内部还可以嵌套requires,假如我们去掉requires,那么它只会检查代码是否合法,比如如果改成are_same_v<Args...>,那么它一直会正常生成,因为代码是没有问题的,而我们想要检查的是它是否真的是一致的。

整个requires expression必须放在requires的字句中,即:

template<typename... Args>
requires requires(Args... args)
{
	(... + args);
	requires are_same_v<Args...>;
	requires sizeof...(Args) > 1;
	{(... + args)} noexcept;
	{(... + args)} -> same_as<first_arg_t<Args...>>;
}

或者,我们把它定义成一个concept

template<typename... Args>
concept Addable = requires(Args... args)
{
	(... + args);
	requires are_same_v<Args...>;
	requires sizeof...(Args) > 1;
	{(... + args)} noexcept;
	{(... + args)} -> same_as<first_arg_t<Args...>>;
};

然后

template<typename... Args>
requires Addable<Args...>
auto Add(Args&... args){
	return (... + args);
}

Test

那我们来测试一下

template<bool nexcept, bool operatorPlus, bool validReturnType>
struct Stub{
	Stub operator+(const Stub& rhs) noexcept(nexcept) 
		requires(operatorPlus && validReturnType)
	{return *this;}
	
	//operatorPlus with invalid return type
	int operator+(const Stub& rhs) noexcept(nexcpet)
		requires(operatorPlus && not validReturnType)
	{return {};}
};

using NoAdd = Stub<true,false,true>;
using ValidClass = Stub<true,true,true>;
using NotNoexcept = Stub<false,true,true>;
using DifferentReturnType = Stub<true,true,false>;

我们定义一个结构Stub用来测试,分别用三个bool值控制结构的性质,是否有operator+(),它是否是noexcept,或者返回值是否valid。然后using给各类型起个别名。

然后就来测试吧。

static_assert(not Addable<int,double>);
static_assert(not Addable<int>);
static_assert(not Addable<NoAdd,Noadd>);

static_assert(Addable<ValidClass,ValidClass>);
static_assert(Addable<int,int>);

缩写

concept的使用可以缩进参数中。

template<typename T>
void DoLock(T&& f){
	std::lock_guard lock{GlobalOsMutex};
	
	f();
} //c++17
void DoLock(std::invocable auto&& f){
	std::lock_guard lock{GlobalOsMutex};
	
	f();
}

auto的参数c++会自动生成模板。

报错信息

concepts的报错信息相当友好,平常使用过模板,发生错误的人都知道,一旦你的错误条道了模板里,在极端情况下可能会生成上百条错误信息,基本宣告放弃了。

concepts会直接指出你的错误发生在哪里,往往是不满足concepts的约束发生的。

非类型模板参数

c++20新增的关于非类型模板参数的特征有:

  • 你可以使用float或double作为非类型模板参数

  • 非类型模板参数有这样一种用法,我不知道怎么形容:

    template<typename T>
    struct Non_Type{
    	T data;
    	constexpr Non_Type(T t):data(t){}
    };
    
    template<Non_Type U>
    struct Non_TypeContainer{
    	auto get(){return U.data;}
    };
    
    Non_TypeContainer<2022> v;
    

    注意我们直接使用2022当作参数传递给Non_TypeContainer,不过这里它会把2022自动转成非类型模板参数Non_Type的。

lambda

lambda可以使用模板了

auto max = []<typename T>(T x,T y){
	return (x > y)> x: y;
};

以上,为演讲内容。

posted @ 2022-06-14 21:37  ᴮᴱˢᵀ  阅读(84)  评论(0)    收藏  举报