C++ Concept

补一个 C++20的特性Concept,用于约束模板参数。 Concept 本质上是一种约束,用于限制模板参数的类型。

本文希望你使用C++ 20标准

希望根据你的编译器添加-std=c++20的参数,例如

$ g++ file.cc -std=c++20 # for g++
$ clang++ file.cc -std=c++20 # for clang++

制作一个判断迭代器是否存在的方法

上篇谈到的C++中模板的基本使用技巧 中,我们提到在C++ 20以前如何判断一个类型 是否具有迭代器,我们可以使用SFINAE技巧,和括号表达式的方法对应有类型进行判断,但是这种比较难看😵。

template <typename T>
struct is_iterable
{
    using type = decltype((
        begin(std::declval<T&>()) !=
            end(std::declval<T&>()), // begin/end and operator !=
        void(),                      // Handle evil operator ,
        ++std::declval<decltype(begin(std::declval<T&>()))&>(), // operator++
        void(*begin(std::declval<T&>())),                       // operator*
        std::true_type{}));
};

使用Concept

一个简单的加和例子

先来个简单的例子,我们该如何使用Concept来约束来个数相加的函数?

template <typename T>
T add(T a, T b) { return a + b; }

这里我们几乎可以传入任何类型的参数,同时也会保证T类型支持+操作符(不支持就直接报错了)。 但是我们几乎只能靠自己来判断T是否支持+操作符,否则只能等报错。在C++ 20之前,如果我们希望模板参数是整数, 我们会使用SFINAE

/// C++ 17
template < typename T, typename = std::enable_if_t< std::is_integral_v< T > > >
T add(T a, T b) {
    return a + b;
}

int main() {
    add(1.1, 2); //报错
    add(11, 2);
}

Clang给出了一个不错的报错提示

2.cc:9:5: error: no matching function for call to ‘add’ 9 | add(1.1, 2);
| ^~~
2.cc:4:3: note: candidate template ignored: deduced conflicting types for parameter ‘T’ (‘double’ vs. ‘int’)
4 | T add(T a, T b) {
| ^
1 error generated.

但当我使用gcc的时候,嗯….😇至少我是很难受的。

2.cc: In function ‘int main()’:
2.cc:9:8: 错误:对‘add(double, double)’的调用没有匹配的函数 9 | add(1.1, 2.2); | ~~~^~~~~~~~~~
2.cc:4:3: 附注:备选: ‘template<class T, class> T add(T, T)’ 4 | T add(T a, T b) { | ^~~
2.cc:4:3: 附注: template argument deduction/substitution failed: In file included from 2.cc:1: /usr/include/c++/14.2.1/type_traits: In substitution of ‘template<bool _Cond, class _Tp> using std::enable_if_t = typename std::enable_if::type [wi th bool _Cond = false; _Tp = void]’:
2.cc:3:24: required from here 3 | template < typename T, typename = std::enable_if_t< std::is_integral_v< T > > > | ^~~~~~~~ /usr/include/c++/14.2.1/type_traits:2696:11: 错误:no type named ‘type’ in ‘struct std::enable_if<false, void>’ 2696 | using enable_if_t = typename enable_if<_Cond, _Tp>::type; | ^~~~~~~~~~~

使用Concept

先看代码

/// C++ 20
template < typename T > concept Integral = std::is_integral_v< T >;

template < Integral T >
T add(T a, T b) {
    return a + b;
}

直觉上来说,我们大概是定义了一个类型?Integral,这个类型是std::is_integral_v< T >的类型?然后我们要求 在add函数中,T必须是Integral类型。

好像还挺理解的。先看gcc的报错

2.cc: In function ‘int main()’:
2.cc:11:8: 错误:对‘add(double, double)’的调用没有匹配的函数
11 | add(1.1, 2.2);
| ~~~^~~~~~~~~~
2.cc:6:3: 附注:备选: ‘template requires Integral T add(T, T)’ 6 | T add(T a, T b) {
| ^~~
2.cc:6:3: 附注: template argument deduction/substitution failed:
2.cc:6:3: 附注:constraints not satisfied
2.cc: In substitution of ‘template requires Integral T add(T, T) [with T = double]’:
2.cc:11:8: required from here
11 | add(1.1, 2.2);
| ~~~^~~~~~~~~~
2.cc:3:33: required for the satisfaction of ‘Integral’ [with T = double]
2.cc:3:49: 附注:the expression ‘is_integral_v [with T = double]’ evaluated to ‘false’
3 | template < typename T > concept Integral = std::is_integral_v< T >;
| ~~~~~^~~~~~~~~~~~~~~~~~

好像还挺友好的,至少我知道了double不是Integral类型,还告诉了我add没有匹配的函数。同时还能看到 Integral<T> [with T = double]的表达式std::is_integral_v< T >的值是false

简单来说Concept是一种约束,用于限制模板参数的类型。在上面这个例子中,我们定义一个concept并且要求其模板参数必须 对std::is_integral_v要为true,才能使用这个函数。

Concept里仍然支持|| &&逻辑运算,所以如果我们想约束多种类型,我们可以使用|| or &&来连接。 例如我们想要约束整数和浮点数类型。这样我们的参数只支持所有整数和浮点数类型。

template < typename T >
concept IntOrFloat = std::is_integral_v< T > || std::is_floating_point_v< T >;

使用requires

concept的复杂语句离不开requires的使用,我们继续is_iterable的例子。在requires语句里面,我们可以写任何语句, concept的要求只是,对于requires的语句必须成立(也就是该表达式存在)。

template < typename T >
concept is_iterable = requires(T t) {
    t.begin() != t.end();
    ++t.begin();
    *t.begin();
};

template < is_iterable T >
void print(T t) {
    for (auto i : t) {
        std::cout << i << std::endl;
    }
}

int main() {
    std::vector< int > vec{1, 2};
    print(vec);
    return 0;
}

在上面例子中,我们的requires语句里面使用了一个T t,好像是一个变量,然后requires语句体里面则写了一堆表达式。 在requires语句中,我们凭空定义一个T类型的t,然后要求这个t必须支持beginend方法,以及++*操作符。

requires 的 requires

requires语句里面还可以使用requires语句,这样我们可以更加灵活的使用Concept

template < typename T >
concept is_iterable = requires(T t) {
    t.begin() != t.end();
    ++t.begin();
    *t.begin();
    requires requires(T t) { // requires 的 requires
        t.size();
    };
};

template < typename T >
    requires is_iterable< T >
void print(T t) {
    for (auto i : t) {
        std::cout << i << std::endl;
    }
}

int main() {
    std::vector< int > vec{1, 2};
    print(vec);
    return 0;
}

我们多了一条requires语句,这条语句其实是要求T类型必须支持size方法。 不过我们居然requires 套 requires。我的理解是requires语句本身构成了一个concept, 而我们本来也可以对concept进行requires的操作。

template < typename T >
    requires is_iterable< T >
void print(T t) {
    for (auto i : t) {
        std::cout << i << std::endl;
    }
}

多concept组合

我们将上面定义的is_iterableIntOrFloat组合起来,定义一个is_intger_iterable。 同时还要求第一个参数是模板类。只使用一个模板参数。但是在print函数中,我们可以支持模板类使用多个模板参数。 这是由于标准库的std::vector有多个模板参数,除第一个参数外,还有一个std::allocator的模板参数,只不过以默认参数的形式存在。

template < template < typename > class T, typename U >
concept is_intger_iterable = requires(T< U > t) {
    requires is_iterable< T< U > >;
    requires IntOrFloat< U >;
};

template < template < typename... > class T, typename U >
void print(T< U > t) {
    for (auto i : t) {
        std::cout << i << std::endl;
    }
}

int main() {
    std::vector< int > vec{1, 2};
    print< std::vector, int >(vec);
    print(vec);
    return 0;
}