SFINE(substitution failure is not an error)
在模板编程中,SFINE是比较常见的一种特性,举个例子【1】:
template<typename T, unsigned int N>
std::size_t GetArrayLen(T(&)[N])
{
return N;
}
template<typename T, unsigned int N>
T::SizeType GetArrayLen(const T& t)
{
return t.Size();
}
int main()
{
int array[10];
GetArrayLen(array);
return 0;
}
在第二个版本函数中要求T中存在SizeType类型即:
struct Test {
using SizeType = int;
...
};
通过数组array进行模板实参替换时,编译器发现第二个函数模板实参替换后不符合要求(缺少T::SizeType),但不会报错而寻找其他匹配的函数,在所有候选者都不满足要求之后才会出现编译报错:no matching function for call GetArrayLen,配合std::enable_if进而过滤函数模板或者部分特化类模板;
We say "we SFINE out a function" if we mean to apply the SFINE mechanism to ensure that function templates are ignored for certain constraints by instrumenting the template code to result in invalid code for these constraints.
示例:判断类型是否支持前置自增和后置自增
方式一
namespace is_incrementable_
{
// a type returned from operator++ when no increment is found in the
// type's own namespace
struct tag {};
// any soaks up implicit conversions and makes the following
// operator++ less-preferred than any other such operator that
// might be found via ADL.
struct any { template <class T> any(T const&); };
tag operator++(any const&);
tag operator++(any const&,int);
// In case an operator++ is found that returns void, we'll use ++x,0
tag operator,(tag,int);
// two check overloads help us identify which operator++ was picked
char (& check_(tag) )[2];
template <class T>
char check_(T const&);
template <class T>
struct impl
{
static typename boost::remove_cv<T>::type& x;
BOOST_STATIC_CONSTANT(
bool
, value = sizeof(is_incrementable_::check_(BOOST_comma(++x,0))) == 1
);
};
template <class T>
struct postfix_impl
{
static typename boost::remove_cv<T>::type& x;
BOOST_STATIC_CONSTANT(
bool
, value = sizeof(is_incrementable_::check_(BOOST_comma(x++,0))) == 1
);
};
}
template<typename T>
struct is_incrementable :
public boost::integral_constant<bool, boost::detail::is_incrementable_::impl<T>::value>
{
};
template<typename T>
struct is_postfix_incrementable :
public boost::integral_constant<bool, boost::detail::is_incrementable_::postfix_impl<T>::value>
{
};
实现方式来自boost库【2】,在is_incrementable_::impl进行模板实参替换时:
1)x 支持x++或者++x,则逗号表达式最终为0,匹配至
template <class T>
char check_(T const&);
返回类型为char,则value = sizeof(char) == 1,为true;
2)x不支持时,则匹配到tag operator++(any const&);,其中any的构造函数能支持任意类型,返回为tag;
再匹配到逗号表达式重载函数声明tag operator,(tag,int);,逗号表达式最终为tag类型,此时check_有模板函数和普通函数都能满足匹配,编译器优先选择普通函数,即
char (& check_(tag) )[2];
得到value = sizeof(char(&)[2]) == 1,为false;
方式二
template<typename T, typename = std::void_t<>>
struct IsIncrementable : std::false_type {};
template<typename T>
struct IsIncrementable<T, std::void_t<decltype(++std::declval<T&>())>> : std::true_type {};
template<typename T, typename = std::void_t<>>
struct IsPostIncrementable : std::false_type {};
template<typename T>
struct IsPostIncrementable<T, std::void_t<decltype(std::declval<T&>()++)>> : std::true_type {};
template<typename T, typename = std::enable_if_t<IsIncrementable<T>::value && IsPostIncrementable<T>::value>>
void Process(T t)
{
std::cout << "T is incrementable " << std::endl;
}
struct TestInc {
TestInc& operator++()
{
return *this;
}
TestInc operator++(int)
{
return *this;
}
};
struct Test {
};
int main()
{
Process(TestInc {});
// Process(Test {}); not found
return 0;
}
Process对模板参数有约束,要求支持前置自增和后置自增,对于不支持自增操作的类型,IsIncrementable利用SFINE过滤掉了特化版本,选择了泛化版本,即继承了std::false_type;
相较于方式一中is_incrementable的实现有所简化;
方式三
C++20增加的4大特性之一,Constraits and concepts为模板编程带来了极大的便利,利用concepts实现:
template<typename T>
concept IsIncmentable = requires(T t) {
t++;
++t;
};
template<IsIncmentable T>
void Process(T t)
{
std::cout << "T is incrementable " << std::endl;
}
struct TestInc {
TestInc& operator++()
{
return *this;
}
TestInc operator++(int)
{
return *this;
}
};
struct Test {
};
int main()
{
Process(TestInc {});
// Process(Test {}); // not found
return 0;
}
进一步减少代码行数,Process函数模板可以修改为:
template<typename T> requires requires (T t) {t++; ++t;}
void Process(T t)
{
std::cout << "T is incrementable " << std::endl;
}
Constraits and concepts
Class templates, function templates, and non-template functions (typically members of class templates) may be associated with a constraint, which specifies the requirements on template arguments, which can be used to select the most appropriate function overloads and template specializations. 【3】
concept语法:
template<模板参数列表>
concept 概念名 = 约束表达式;
requires语法:
requires (形参列表) {
表达式;
}
示例:函数模板参数满足为指针类型的约束
template<typename T>
concept IsPointer = std::is_pointer_v<T>; // 概念约束模板参数为指针类型
以下四种语法表达均符合要求:
template<IsPointer T> // 按语境推导出的类型会隐式地用作第一个实参,即IsPointer<T>
void Process(T p) { }
template<typename T> requires IsPointer<T> // requires 子句
void Process(T p) { }
template<typename T>
void Process(T p) requires IsPointer<T> { } // requires 子句
void Process(IsPointer auto p) { }
合取(conjunction)与析取(disjunction)
合取与析取分别对应&&和||符号,在依次对每个约束进行检查时,首先检查表达式是否合法,若不合法则该约束不满足,否则进一步对约束进行求值判断是否满足【3】
template<typename T, typename U>
concept C = (std::is_integral_v<typename T::ValueType> || std::is_integral_v<typename U::ValueType>); // 析取表达式
struct MyType {
using ValueType = int;
};
static_assert(C<double, MyType>);
double没有ValueType,表达式不合法;而第二个表达式合法,所以约束为真;
template<typename T, typename U>
concept C1 = bool(std::is_integral_v<typename T::ValueType> || std::is_integral_v<typename U::ValueType>); // 非析取表达式
static_assert(!C1<double, MyType>);
此处C2并不是析取表达式,首先检查整个表达式是否合法,double中没有ValueType则整个表达式为假;
template<typename ...T> // 非析取表达式
concept C1 = (std::is_integral_v<typename T::ValueType> || ...);
对于可变参模板的约束表达式,同样只要有一个类型没有ValueType,则整个表达式为假;
requires表达式
requires表达式的返回类型为bool,总结为:
template<typename T>
concept C = (traits) && (contexpr bool 值或函数) && (概念约束) &&
requires(形参列表) {
表达式;// 只检查表达式有效性
{ 表达式;} -> 返回值需要满足的概念约束
} &&
requires { // 嵌套
requires bool表达式; // 即检查表达式合法性也求值
}
嵌套实现中使用requires表达式时需要添加requires关键字
requires {
requires (形参列表) { 表达式;}; // compile error
}
修改为:
requires { // 嵌套
requires requires (形参列表) { 表达式;};
}
requires子句
关键词 requires 用来引入 requires 子句,它指定对各模板实参,或对函数声明的约束。 即上文示例中的函数模板参数约束:
template<typename T> requires IsPointer<T> // requires 子句
void Process(T p) { }
在上文判断类中支持自增的方式三实现中,即使用require + require表达式:
template<typename T> requires requires (T t) {t++; ++t;}
void Process(T t)
{
std::cout << "T is incrementable " << std::endl;
}
使用requires子句对函数模板约束时更加方便,示例:
template<typename T>
void Process(T) {} // 通用实现
template<typename T, typename = std::enable_if_t<sizeof(T) == 2>>
void Process(T) {}
上述实现中当调用Process((short)1);,两个函数模板在重载决议中会存在歧义报错;
template<typename T> requires (sizeof(T) == 2)
void Process(T) {}
template<typename T>
void Process(T) {}
修改为requires 子句约束后,则能选择到约束版本;
约束偏序
concept会对约束表达式中的各个约束进行规范化展开,直到剩下不可分割约束(原子约束);
template<typename T>
concept Con1Concept = std::is_integral_v<T>;
template<typename T>
concept Con2Concept = std::is_integral_v<T> && sizeof(T) > 1;
void Process(Con1Concept auto t)
{
}
void Process(Con2Concept auto t)
{
}
上述代码中两个concept为原子约束,相互之间没有包含关系,因此在Process((int)0)编译时,两个函数都能满足,编译器无法决议使用哪个函数导致歧义,将上述代码修改为:
template<typename T>
concept Con0Concept = std::is_integral_v<T>;
template<typename T>
concept Con1Concept = Con0Concept<T>;
template<typename T>
concept Con2Concept = Con0Concept<T> && sizeof(T) > 1;
此时Con1Concept的原子约束为Con0Concept,而Con2Concept包含Con0Concept,即Con2Concept包含Con1Concept,即Con2Concept更受约束;因此在Process((int)0)编译时,两个重载函数在进行决议时选择更受约束的版本;
注意事项
concept不 能 被 继 承
概念不能递归地提及自身
template<typename T>
concept V = V<T*>; // 错误:递归的概念
不 能 通 过 requires限 定 一 个 concept 【4】
template<class T>
concept C1 = true;
template<C1 T>
concept Error1 = true; // 错误:C1 T 试图约束概念定义
template<class T> requires C1<T>
concept Error2 = true; // 错误:requires 子句试图约束概念
不 能 通 过 new分 配
不 能 用 于约束 虚 函 数 (虚 函 数 不 支 持 模 板 )
参考资料
【1】C++ templates
【2】boost/detail/is_incrementable.hpp
【3】C++20高级编程
【4】https://en.cppreference.com/w/cpp/language/constraints
标签:std,Process,介绍,约束,++,表达式,template,requires,模板 From: https://blog.51cto.com/u_13137973/6131201