编辑推荐:
本文主要介绍了C++20详解:Concept相关知识。 希望能为大家提供一些参考或帮助。
文章来自于知乎,由火龙果Linda编辑推荐。
C++20相较于之前的C++11等标准有了很大的变化,在这一系列文章当中,我将详细介绍C++20的各种特性,本文介绍的是C++20中新出现的
concept。
Motivation
在C++20之前,我们有两种截然相反的方法来思考函数或类:为特定类型定义函数或类,或者为泛型类型定义函数或类。在后一种情况下,我们称它们为函数模板或类模板。
C++的 Template 为编程带来的极大的方便,但是也会存在各种各样的问题,举个简单的例子,假设我们希望为两个数的加法封装一个函数,为了使其能够支持更多的类型,我们将其定义为函数模板:
template <typename T>
auto add(T first, T second)
{
return first + second;
}
但是这样也会带来新的麻烦,那就是可能会有一些意料之外的情况出现,比如:
当编译器看到这一语句时,虽然能够进行模板实例化:
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int add<bool>(bool first, bool second)
{
return static_cast <int>(first) + static_cast<int>(second);
}
#endif
但是这种将 bool 类型提升为 int 类型的解决方法,可能和我们的期待并不相符,而在C++20之前,Template的这一弊端并不容易解决。
Advantages
将模板参数的要求(Requirements)变为接口的一部分
函数的重载和类模板的实例化可以基于 concept。
concept 可用于函数模板、类模板、类或类模板的泛型成员函数。
编译器会将模板参数的要求与给定的模板参数进行比较,从而得到更直观的错误消息。
您可以使用预定义的 concept,也可以定义自己的 concept。
auto 和 concept 的使用是统一的。你可以用一个 concept 来代替auto。
如果函数声明使用 concept,它将自动成为函数模板。因此,编写函数模板与编写函数一样简单
Use
concept使用起来十分方便,具体来讲有四种方式:
requires 语句
后缀 requires 语句
受约束的模板参数
简化的函数模板
下面给出一个简单的例子:
#include <concepts>
#include <type_traits>
#include <iostream>
template <typename T>
requires std::integral<T>
T gcd(T a, T b)
{
if (b == 0)
return a;
else
return gcd(b, a % b);
}
template <typename T>
T gcd1(T a, T b) requires std::integral<T>
{
if (b == 0)
return a;
else
return gcd1(b, a % b);
}
template <std::integral T>
T gcd2(T a, T b)
{
if (b == 0)
return a;
else
return gcd2(b, a % b);
}
std::integral auto gcd3(std::integral auto a, std::integral auto b)
{
if (b == 0)
return a;
else
return gcd3(b, a % b);
}
int main()
{
std::cout << "gcd(100, 10)= " << gcd(100, 10) << '\n';
std::cout << "gcd1(100, 10)= " << gcd1(100, 10) << '\n';
std::cout << "gcd2(100, 10)= " << gcd2(100, 10) << '\n';
std::cout << "gcd3(100, 10)= " << gcd3(100, 10) << '\n';
}
其中,要想使用 concept,我们首先要包含头文件 <concepts>,这个头文件可以让我们使用
std::integral 等 concept。
requires 语句的更多用法
除了上面例子中使用特定 concept 来定义函数或函数模板外,使用 requires 语句还可以实现更多的功能。
关键字 requires 可以指定对模板参数(gcd)或函数声明(gcd1)的约束。requires
后面必须跟编译时谓词,如命名 concept、命名 concept 的合取/析取或requires
表达式(Requires Expressions)或者编译期布尔表达式:
#include <iostream>
template <unsigned int i>
requires (i <= 20)
int sum(int j)
{
return i + j;
}
int main()
{
std::cout << "sum<20>(2000): " << sum<20>(2000) << '\n';
}
但是事实上,我们并不是很推荐这样的做法。一个更好地做法是使用命名 concept 或它们的组合,通过对
concept 进行命名可以实现它们的重用,具体方法我们将在后面进行介绍。
什么是 concept
concept 归根结底就是编译时谓词。而编译时谓词就是在编译时执行并返回布尔值的函数。在深入研究
concept 的各种用例之前,我们必须要揭开 concept 的神秘面纱,并将它们简单地表示为在编译时返回布尔值的函数,我们只需要一个简单的例子就能很好地理解:
struct Test{};
int main()
{
std::cout << '\n';
std::cout << std::boolalpha;
std::cout << "std::three_way_comparable<int>: "
<< std::three_way_comparable<int> << "\n";
std::cout << "std::three_way_comparable<double>: ";
if (std::three_way_comparable<double>)
std::cout << "True";
else
std::cout << "False";
std::cout << "\n\n";
static_assert (std::three_way_comparable<std::string>);
std::cout << "std::three_way_comparable<Test>: ";
if constexpr (std::three_way_comparable<Test>)
std::cout << "True";
else
std::cout << "False";
std::cout << '\n';
std::cout << "std::three_way_comparable<std::vector<int>>: ";
if constexpr (std::three_way_comparable<std::vector <int>>)
std::cout << "True";
else
std::cout << "False";
std::cout << '\n';
}
其中,std::three_way_comparable可以在编译时检查 T 是否支持六个比较运算符,程序运行结果如下:
std::three_way_comparable<int>: true
std::three_way_comparable<double>: True
std::three_way_comparable<Test>: False
std::three_way_comparable<std::vector<int>>: True
任意数量模板参数
concept同样支持任意数量模板参数,使用起来同样非常简单:
template<std::integral... Args>
bool all (Args... args) { return (... && args); }
template<std::integral... Args>
bool any (Args... args) { return (... || args); }
template<std::integral... Args>
bool none (Args... args) { return not(... || args); }
int main()
{
std::cout << std::boolalpha << '\n';
std::cout << "all(5, true, false): " << all(5, true, false) << '\n';
std::cout << "any(5, true, false): " << any(5, true, false) << '\n';
std::cout << "none(5, true, false): " << none(5, true, false) << '\n';
}
重载示例
std::advance 是标准模板库的一种算法,它将给定的迭代器 iter 递增 n 个元素。根据给定迭代器的功能,可以使用不同的高级策略。例如,std::forward_list
支持只能单向前进的迭代器,而 std::list 支持双向迭代器,std::vector 支持随机访问迭代器。因此,对于
std::forward_list 或 std ::list 提供的迭代器,对 std::advance(iter,
n) 的调用必须递增n次。 而对于由 std::vector 提供的 std::random_access_iterator,这种时间复杂性不成立。
那么,对于这几种明显不同的迭代器,C++也提供了对应的 concept,我们使用一个简单的例子对其进行验证:
template <std::forward_iterator I>
void advance(I &iter, int n)
{
std::cout << "forward_iterator" << '\n';
}
template <std::bidirectional_iterator I>
void advance(I &iter, int n)
{
std::cout << "bidirectional_iterator" << '\n';
}
template <std::random_access_iterator I>
void advance(I &iter, int n)
{
std::cout << "random_access_iterator" << '\n';
}
int main()
{
std::cout << '\n';
std::forward_list forwList{1, 2, 3};
std::forward_list <int>::iterator itFor = forwList.begin();
advance(itFor, 2);
std::list li{1, 2, 3};
std::list<int>::iterator itBi = li.begin();
advance(itBi, 2);
std::vector vec {1, 2, 3};
std::vector<int>::iterator itRa = vec.begin ();
advance(itRa, 2);
std::cout << '\n';
}
编译器会自动选择合适的重载:
forward_iterator
bidirectional_iterator
random_access_iterator
显式模板实例化
concept同样支持显式的模板实例化:
template <typename T>
struct Vector
{
Vector()
{
std::cout << "Vector<T>" << '\n';
}
};
template <std::regular Reg>
struct Vector<Reg>
{
Vector()
{
std::cout << "Vector<std::regular>" << '\n';
}
};
占位符
concept 可以用作返回类型、基于范围的for循环、变量类型。
std::integral auto getIntegral(int val)
{
return val;
}
int main()
{
std::cout << std::boolalpha << '\n';
std::vector<int> vec{1, 2, 3, 4, 5};
for (std::integral auto i : vec)
std::cout << i << " ";
std::integral auto b = true;
std::cout << b << '\n';
std::integral auto integ = getIntegral(10);
std::cout << integ << '\n';
auto integ1 = getIntegral(10);
std::cout << integ1 << '\n';
std::cout << '\n';
}
使用多于一个concept
截至目前,我们介绍的 concept 的用法都很简单,但大多数情况下,同时使用的 concept 往往不止一个,这时候使用布尔运算符将不同的条件连接起来即可。
template<typename Iter, typename Val>
requires std::input_iterator<Iter>
&& std::equality_comparable<Value_type<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
find 要求迭代器 Iter 及其与 val 的比较满足以下两个条件:
迭代器必须是输入迭代器
迭代器的值类型必须与val相等。
当然,我们也可以等价表示为受约束的模板参数:
template<std::input_iterator Iter, typename Val>
requires std::equality_comparable<Value_type<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
缩写函数模板
在C++20中,既可以在函数声明中使用不受约束的占位符(auto),也可以使用约束的占位符(concept),并且此函数声明会自动成为函数模板。
template <typename T>
requires std::integral<T>
T gcd(T a, T b)
{
if (b == 0)
return a;
else
return gcd(b, a % b);
}
template <typename T>
T gcd1(T a, T b) requires std::integral<T>
{
if (b == 0)
return a;
else
return gcd1(b, a % b);
}
template <std::integral T>
T gcd2(T a, T b)
{
if (b == 0)
return a;
else
return gcd2(b, a % b);
}
std::integral auto gcd3(std::integral auto a, std::integral auto b)
{
if (b == 0)
return a;
else
return gcd3(b, a % b);
}
auto gcd4(auto a, auto b)
{
if (b == 0)
return a;
return gcd4(b, a % b);
}
int main()
{
std::cout << '\n';
std::cout << "gcd(100, 10)= " << gcd(100, 10) << '\n';
std::cout << "gcd1(100, 10)= " << gcd1(100, 10) << '\n';
std::cout << "gcd2(100, 10)= " << gcd2(100, 10) << '\n';
std::cout << "gcd3(100, 10)= " << gcd3(100, 10) << '\n';
std::cout << "gcd4(100, 10)= " << gcd4(100, 10) << '\n';
std::cout << '\n';
}
函数模板 gcd3 具有作为类型参数的概念 std::integral,因此成为具有受限类型参数的函数模板。相反,gcd4
等同于对其类型参数没有限制的函数模板。在 gcd3 和 gcd4 中用于创建函数模板的语法被称为缩写函数模板语法。
模板匹配优先级
模板匹配的优先级:
完整类型
带约束的 concept
无约束的 auto
void overload(auto t)
{
std::cout << "auto : " << t << '\n';
}
void overload(std::integral auto t)
{
std::cout << "Integral : " << t << '\n';
}
void overload(long t)
{
std::cout << "long : " << t << '\n';
}
int main()
{
std::cout << '\n';
overload(3.14);
overload(2010);
overload(2020L);
std::cout << '\n';
}
编译器对 double 选择 auto 上的重载,对 int 选择 concept std::integral
上的重载,对 long 选择 long 上的重载。
Predefined Concepts
three_way_comparable
该 concept 在 <compare> 中定义。
假设 a 和 b 属于类型 T,则它属于 three_way_comparable 当且仅当:
(a <=> b == 0) == bool(a == b) is true
(a <=> b != 0) == bool(a != b) is true
((a <=> b) <=> 0) and (0 <=> (b
<=> a)) are equal
(a <=> b < 0) == bool(a < b) is true
(a <=> b > 0) == bool(a > b) is true
(a <=> b <= 0) == bool(a <= b) is true
(a <=> b >= 0) == bool(a >= b) is true
Concepts Library
最常见的 concept 都可以在 <concepts> 头文件中找到
Language-related concept
same_as
derived_from
convertible_to
common_reference_with
common_with
assignable_from
swappable
Arithmetic Concepts
integral
signed_integral
unsigned_integral
floating_point
定义:
template<class T>
concept integral = is_integral_v<T>;
template<class T>
concept signed_integral = integral<T> && is_signed_v<T>;
template<class T>
concept unsigned_integral = integral<T> && !signed_integral<T>;
template<class T>
concept floating_point = is_floating_point_v<T>;
Lifetime Concepts
destructible
constructible_from
default_constructible
move_constructible
copy_constructible
Comparison Concepts
equality_comparable
totally_ordered
与数学上的定义一致,对于 T 类型的a,b和c,T类型满足于 totally_ordered 当且仅当
Exactly one of bool(a < b), bool(a > b), or
bool(a == b) is true
If bool(a < b) and bool(b < c), then bool(a
< c)
bool(a > b) == bool(b < a)
bool(a <= b) == !bool(b < a)
bool(a >= b) == !bool(a < b)
Object Concepts
movable
copyable
semiregular
regular
定义:
template<class T>
concept movable = is_object_v<T> && move_constructible<T> &&
assignable_from<T&, T> && swappable<T>;
template<class T>
concept copyable = copy_constructible<T> && movable<T> &&
assignable_from<T&, T&> &&
assignable_from<T&, const T&> && assignable_from<T&, const T>;
template<class T>
concept semiregular = copyable<T> && default_initializable<T>;
template<class T>
concept regular = semiregular<T> && equality_comparable<T>;
movable 首先要求 T 满足 is_object_v<T>。从 is_object_v<T>
的定义上讲,T可以是标量、数组、union或者class。
Callable Concepts
invocable
regular_invocable:可调用、不修改函数参数、给定相同输入时给出相同输出
predicate:可调用、返回值为 bool 类型
Iterators Library
迭代器库有许多重要的 concept。它们在 <iterator> 头中定义。
input_iterator
output_iterator
forward_iterator
bidirectional_iterator
random_access_iterator
contiguous_iterator
下面的关系成立:随机访问迭代器是双向迭代器,双向迭代器是前向迭代器。连续迭代器是随机访问迭代器,要求容器的元素连续存储在内存中。
Algorithm Concepts
permutable:可以就地重新排序元素
mergeable:可以将排序后的序列合并到输出序列中
sortable:将序列置换为有序序列是可能的
Ranges Library
input_range:指定其迭代器类型满足 input_iterator 的范围(例如,可以至少从开始到结束迭代一次)
output_range:指定其迭代器类型满足 output_iterator 的范围
forward_range:指定其迭代器类型满足 forward_iterator 的范围(可以从开始到结束多次迭代)
bidirectional_range:指定其迭代器类型满足 bidirectional_iterator
的范围(可以向前和向后迭代多次)
random_access_range:指定其范围。用索引运算符[] 取得任意元素的时间相同
contiguous_range:指定迭代器类型满足 contiguous_iterator 的范围(元素连续存储在内存中)
std::ranges::contiguous_range 支持剩下三个概念
Defined Concept
concept 的定义以 Template 关键字开头,并具有模板参数列表。第二行使用关键字 concept,后跟
concept 名称和约束表达式,形式如下:
template <template-parameter-list>
concept concept-name = constraint-expression;
其中,约束表达式可以是:
其他 concept 或编译时谓词的逻辑组合
requires 表达式
Simple Requirments
Type Requirements
Compound Requirements
Nested Requirements
例如:
template <typename T>
concept Integral = std::is_integral<T>::value;
template <typename T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;
template <typename T>
concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
requires语句
使用requires表达式,可以定义更强大的concept。requires表达式具有以下形式:
requires (parameter-list(optional)) {requirement-seq}
parameter-list:类似函数声明中的逗号分隔的参数列表
requirement-seq:requirements序列
Simple Requirments
template<typename T>
concept Addable = requires (T a, T b) {
a + b;
};
Addable 这一concept要求相同类型的两个值满足可加性
Type Requirements
在 Type Requirements 中,我们需要将关键字 typename 与类型名称一起使用
template<typename T>
concept TypeRequirement = requires {
typename T::value_type;
typename Other<T>;
};
TypeRequirement 要求类型 T 具有嵌套成员 value_type,并且类模板 Other
可以用 T 实例化。
完整的例子如下:
#include <iostream>
#include <vector>
template <typename>
struct Other;
template <>
struct Other<std::vector<int>>{};
template <typename T>
concept TypeRequirement = requires
{
typename T::value_type;
typename Other<T>;
};
int main()
{
TypeRequirement auto myVec = std::vector<int>{1, 2, 3};
}
Compound Requirements
{expression} noexcept(optional) return-type-requirement(optional);
Compound Requirements 在 Simple Requirments 的基础上,还可以有一个
noexcept 说明符及对其返回类型的要求。
例如:
template <typename T>
concept Equal = requires(T a, T b)
{
{a == b} -> std::convertible_to<bool>;
{a != b} -> std::convertible_to<bool>;
};
bool areEqual(Equal auto a, Equal auto b)
{
return a == b;
}
struct WithoutEqual
{
bool operator ==(const WithoutEqual &other) = delete;
};
struct WithoutUnequal
{
bool operator!=(const WithoutUnequal &other) = delete;
};
int main()
{
std::cout << std::boolalpha << '\n';
std::cout << "areEqual(1, 5): " << areEqual(1, 5) << '\n';
// bool res = areEqual(WithoutEqual(), WithoutEqual()); // Error
// bool res2 = areEqual(WithoutUnequal(), WithoutUnequal()); // Error
std::cout << '\n';
}
Equal 要求其类型参数 T 支持等于和不等于运算符。此外,这两个运算符都必须返回一个可转换为布尔值的值。
Nested Requirements
requires constraint-expression;
Nested Requirements 用于指定类型参数的要求。
例如:
template <typename T>
concept Integral = std::is_integral<T>::value;
template <typename T>
concept SignedIntegral = Integral<T> && std::is_signed<T>::value;
// template <typename T>
// concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
template <typename T>
concept UnsignedIntegral = Integral<T> &&
requires(T)
{
requires !SignedIntegral<T>;
};
int main()
{
UnsignedIntegral auto n = 5u; // works
// UnsignedIntegral auto m = 5; // compile time error, 5 is a signed literal
}
Nested Requirements 通过嵌套实现了 concept 的细分。