第三章–3.3–requires子句

Aki 发布于 2022-11-27 330 次阅读


我们通过concept,requires表达式,constexpr谓词常量或函数及type traits能够定义对类型的谓词,本节将介绍如何应用这些编译器谓词对模板参数添加约束,所有可以用来实例化这个模板的参数必须满足这些约束。

使用一个requires子句可以为一个模板类或者函数模板添加约束,考虑如下代码。

template<typename T>
requires is_integral_v<T> 
T gcd(T a, T b)
{
	return a + b;
}

gcd(1.0,2.0);   //error,因为T是double
gcd(1,2);

模板头中额外的requires子句表达了模板函数应该在什么条件下工作,同样的,它还可以接受一个约束表达式。当我们错误的使用受约束的gcd函数,编译器将产生一个友好的错误信息。

设计requires子句的意图是判断它所约束的声明在某些上下文中是否可行,对于函数模板而言,上下文是在执行重载决议时进行的;对于模板类而言,是在决策合适的特化版本中;对于模板类中的成员函数而言,是决策当显示实例化时是否生成该函数。

我们讨论第一个场景,在重载决议中,考虑如下代码。


template<typename T>
requires is_trivial_v<T>
void f(T)
{
	cout << "1" << endl;
}

template<typename T>
void f(T)
{
	cout << "2" << endl;
}

class Test
{
   public:
};

f(vector<int>{});   //2
f(Test{});          //1

这里提供了两个模板函数f,前者要求类型是平凡的,后者则没有任何约束。当对函数进行调用时,传递一个非平凡对象vector<int>,由于候选集中的第一个可行函数的类型不满足要求,将其从候选集中删除,只剩下一个不受约束的版本,因此重载决议中没有产生歧义,最终输出的结果为2。

这里的关键在于违反约束本身并不是一个错误,除非候选集中没有可行函数了,但那是另一回事。上述情况也可以被看作SFINAE,但我们不需要继续使用enable_if等变通方法。

template<typename T>
enable_if_t<is_trivial_t<T>> f(T)
{
   cout<<"1"<<endl;     // 曾经的元编程技巧enable_if
};


template<typename T>
enable_if_t<!is_trivial_t<T>> f(T)
{
   cout<<"2"<<endl;     // 曾经的元编程技巧enable_if
};

enable_if提供的可行函数的条件必须两两互斥,以避免重载决议上的歧义。而concept本身存在优先级机制,这一机制能避免上述问题,这是重大的改进。


template<typename T>
	requires requires (T obj) { obj.func(); }
void f(const T& obj)
{
	cout << "1" << endl;
}


template<typename T>
void f(const T& obj)
{
	cout << "2" << endl;
}

class Test
{
public:
	void func(){}
};

func(Test{});

上述代码使用requires子句结合requires表达式来实现的约束。如果用户提供的类型拥有成员函数func,那么候选集中两个函数都可行,根据标准,受约束的函数比未受约束的函数更加优,编译器将选择正确的第一个版本;用用户提供的类型没有该成员函数,第一个版本将不符合要求,候选集中仅剩下第二个版本的函数,编译器只能选择它。

从这两个例子中我们能够看到concept特性所带来的优势,它不需要那么多元编程技巧,让新人也能够容易接受,上手,而无需理解变通技巧中涉及的一些隐晦问题。

结合之前学习的,就可以写出下面这样的约束。

class Test
{
public:
	void func(){}
	using type = int;
	string name;
	static int id;
	static void func3(){}
};

template<class T>
concept C = requires(T a)
{
	requires is_same_v<T, Test>;
	requires is_same_v<typename T::type, int>;
    a.func();
	{a.func()}->same_as<void>;
	{a.name}->same_as<string&>;
	T::id;
	T::func3();
	requires !is_trivial_v<T>;
};

template<class T>
requires C<T>
void func(const T& rhs)
{
	cout << "hello,world" << endl;
}

static_assert(C<Test>);

requires子句中的约束表达式也支持对约束进行合取和析取操作。除了通过requires子句引入约束之外,在简单情况下还可以通过更简洁的语法来引入约束。

template<class T,class U>
requires (integral<T> &&integral<U>)
void fun(T a, U b)
{

}

简化后可写成

template<integral T,integral U>
void fun(T a, U b)
{

}

我们可以看到关键字class被替换成了概念integral,对多个模板参数添加约束,将产生一个约束合取表达式,正如原版中的一样。此外,不需要填充概念中的模板参数,根据concept的性质它会自动将模板参数补充到第概念中第一个参数位置,这是type traits做不到的。

另一方面也说明了,不需要通过requires子句也能够施加约束。约束的合取比较容易得到,而约束的析取需要通过requires子句才能得到。