C++固定长度数组作用之一——模板参数类型检查

今天同事问了我一下这段代码中的 Resolve 什么意思,大概看了一下,第一反应这不是个函数指针数组嘛,然而如果是函数指针的话,那么Resolve前面的“&”符号怎么解释呢?
代码如下

template <typename Concept, typename... Args>
struct TModels
{
    template <typename... Ts>
    static char (&Resolve(decltype(&Concept::template Requires<Ts...>)*))[2];

    template <typename... Ts>
    static char (&Resolve(...))[1];

    static constexpr bool Value = sizeof(Resolve<Args...>(0)) == 2;
};

仔细研究了一下,发现Resolve其实是一个函数,返回值是一个 char (&)[2] (也就是长度为2的数组的引用),平时需要返回一个数组时,都是返回一个数组的指针,没想到还可以返回一个指定长度的数组。

那返回一个指定长度的数组有什么意义呢?
1.首先,这段代码其实用来做concepts checks(概念检查),概念检查成功意味着给定的C++表达式格式正确。但不保证该表达式的运行时行为的正确性。
2.简单解释一下代码,首先几个关键字

  • decltype:C++11新增了decltype类型说明符,它的作用是选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
  • concepts:C++20的新特性,其实很早就有各种不同实现并被大家使用。concepts(概念)就是一种编译时谓词,指出一个或多个类型应如何使用。概念给一组要求(Requirements)起一个名字,使用这些要求和概念可以给函数和类模板的参数加上约束(有点类似于C#的where T : XXX)

  • require:用于指定约束,格式为 requires ( 形参列表(可选) ) { 要求序列 }

举个2个简单的例子

example1:
concept can_add = requires(T t, U u) { t + u; };  // 要求参数T 和 U 类型必须满足支持 "+"运算

example2:
template <typename T>
auto requires(const T& Val) -> decltype(-Val); // 要求参数T 可以为负值,比如int,而int*则不可以编译通过,因为指针不能为负值。

3.现在回头看下最初的代码段含义
从使用上,我们这样使用它:

TModels<Concept, [...arguments...]>::Value

参数(arguments)被转发到概念(Concepts)的Requires()函数的模板参数,该函数将尝试编译返回值中的表达式,并利用SFINAE(Substitution Failure Is Not An Error)来测试该表达式是否成功。

//#1
template <typename... Ts>
static char (&Resolve(decltype(&Concept::template Requires<Ts...>)*))[2];
//#2
template <typename... Ts>
static char (&Resolve(...))[1];

static constexpr bool Value = sizeof(Resolve<Args...>(0)) == 2;

如果模板推导在#1上失败,则Value 为false,如果推导成功,则Value 为true,所以可以通过使用Value的值来判断模板参数是否合法
使用举例:

// 定义一个可以为负的类型
struct CNegatable
{
    template <typename T>
    auto Requires(const T& Val) -> decltype(
        -Val
    );
};

static_assert( TModels<CNegatable, int >::Value); // ints 可以为负
static_assert(!TModels<CNegatable, int*>::Value); // 指针不能为负