简介
在使用 STL 库的时候, 我们经常会遇到编译器的错误提示冗长且难以理解的情况. 这是因为编译器对模板的编译是分为两步, 第一步是模板的实例化, 使用用户提供的类型去替代模板参数, 第二步是对实例化后的代码进行编译. 编译器报告错误的时候往往是在第二步.
C++20 引入了 Concepts, 它是一种对模板进行约束的机制. Concept 可以用在函数模板(Function Template), 类模板(Class Template), 通用函数成员(Generic Member Function)上. 能对模板参数, 函数参数进行约束. 约束作为接口的一部分, 允许编译器对其进行检查, 能让编译器更早发现错误, 提供更好的错误信息.
如何使用 Concepts
有四种方式使用concepts:
requires语句- 尾部的
requires语句 - 受约束的模板参数
- 函数模板缩写
#include <concepts>
#include <iostream>
using namespace std;
template <typename T>
requires integral<T> // Requires clause
auto max1(T a, T b) {
return a >= b ? a : b;
}
template <typename T>
auto max2(T a, T b)
requires integral<T> // Trailing requires clause
{
return a >= b ? a : b;
}
template <integral T> // Constrained template parameter
auto max3(T a, T b) {
return a >= b ? a : b;
}
auto max4(integral auto a, // Abbreviated function
integral auto b) // template
{
return a >= b ? a : b;
}
int main() {
cout << "max1(1,2) = " << max1(1, 2) << '\n';
cout << "max2(1,2) = " << max2(1, 2) << '\n';
cout << "max3(1,2) = " << max3(1, 2) << '\n';
cout << "max4(1,2) = " << max4(1, 2) << '\n';
}
代码输出:
max1(1,2) = 2
max2(1,2) = 2
max3(1,2) = 2
max4(1,2) = 2
Concepts 使用场景
Concepts 的使用场景有很多, 下面是一些常见的使用场景.
编译时谓词(Compile Time Predicates)
#include <iostream>
using namespace std;
template <typename T>
void checkIntegral(T value) {
if constexpr (integral<T>) { // compile-time predicate
cout << "The value is integral.\n";
} else {
cout << "The value is not integral.\n";
}
}
int main() {
checkIntegral(5); // The value is integral.
checkIntegral(5.5); // The value is not integral.
return 0;
}
类模板(Class Template)
#include <concepts>
template <std::regular T> // class template
class Container {
public:
void push_back(const T& item) {}
};
int main() {
Container<int> a; // OK
Container<int&> b; // ERROR: constraints not satisfied
}
成员函数模板
#include <concepts>
template <typename T>
struct Container {
void push_back(const T&)
requires std::copyable<T> // generic member function
{}
};
struct NotCopyable {
NotCopyable() = default;
NotCopyable(const NotCopyable&) = delete;
};
int main() {
Container<int> a;
a.push_back(2020);
Container<NotCopyable> b; // OK
b.push_back(NotCopyable()); // ERROR, requires copyable
}
可变参数模板
#include <concepts>
#include <iostream>
template <std::integral... Args>
bool all_positive(Args... args) {
return (... && (args > 0));
}
template <std::integral... Args>
bool any_positive(Args... args) {
return (... || (args > 0));
}
template <std::integral... Args>
bool none_positive(Args... args) {
return not(... || (args > 0));
}
int main() {
std::cout << std::boolalpha;
std::cout << " all positive: " << all_positive(-1, 0, 1) << '\n';
std::cout << " any positive: " << any_positive(-1, 0, 1) << '\n';
std::cout << "none positive: " << none_positive(-1, 0, 1) << '\n';
}
执行输出:
all positive: false
any positive: true
none positive: false
重载
#include <concepts>
#include <iostream>
using namespace std;
void fun(auto t) { cout << "fun(auto) : " << t << '\n'; }
void fun(long t) { cout << "fun(long) : " << t << '\n'; }
void fun(integral auto t) { cout << "fun(integral auto) : " << t << '\n'; }
int main() {
fun(2020.);
fun(2020L);
fun(2020);
}
执行输出:
fun(auto) : 2020
fun(long) : 2020
fun(integral auto) : 2020
模板特化(Template Specialization)
#include <concepts>
#include <iostream>
using namespace std;
template <typename T>
struct Container {
Container() { cout << "Use Container<T>" << '\n'; }
};
template <regular T>
struct Container<T> {
Container() { cout << "Use Container<std::regular>" << '\n'; }
};
int main() {
Container<int> a;
Container<int&> b;
}
执行输出:
Use Container<std::regular>
Use Container<T>
使用多个 Concepts
#include <concepts>
#include <iostream>
#include <iterator>
#include <list>
#include <vector>
using namespace std;
template <typename Iter, typename Val>
requires input_iterator<Iter> && // use multi concepts
same_as<typename iterator_traits<Iter>::value_type, Val>
bool contains(Iter b, Iter e, Val v) {
while (b != e && *b != v) ++b;
return b != e;
}
int main() {
vector vec{1, 2, 3, 4, 5};
cout << boolalpha;
cout << contains(vec.begin(), vec.end(), 5) << endl; // true
list list{1.1, 2.2, 3.3, 4.4, 5.5};
cout << contains(list.begin(), list.end(), 5.5) << endl; // true
return 0;
}
受限的和不受限的auto
C++14 增加了泛型 lambda(generic lambda), 但是不支持在函数模板中使用 auto.
所谓泛型 lambda 是指使用auto而不是具体的类型来定义 lambda 函数.
不受限的 auto
单纯的 auto 被称为不受限的占位符,
下面的例子展示了不受限的占位符与相对应的模板函数.
#include <cassert>
template <typename L, typename R>
auto max_tmpl(L l, R r) {
return l >= r ? l : r;
}
auto max_auto(auto l, auto r) { return l >= r ? l : r; }
int main() {
assert(max_auto(1, 2) == max_tmpl(1, 2));
assert(max_auto(-1, 0) == max_tmpl(-1, 0));
}
受限的 auto
受限的占位符是指使用 concepts来约束auto的类型.
下面的例子展示了受限的占位符与相对应的模板函数.
#include <cassert>
#include <concepts>
using namespace std;
template <integral L, integral R>
integral auto max_tmpl(L l, R r) {
return l >= r ? l : r;
}
integral auto max_auto(integral auto l, integral auto r) {
return l >= r ? l : r;
}
int main() {
assert(max_auto(1, 2) == max_tmpl(1, 2));
assert(max_auto(-1, 0) == max_tmpl(-1, 0));
}
Concepts 库简介
一些常用的 Concept 定义在<concepts>头文件中.
语言相关的 concepts
std::same_as<T, U>: 当且仅当T和U是相同的类型时, 该 concept 才为真.std::derived_from<T, U>: 当且仅当T是U的派生类时, 该 concept 才为真.std::convertible_to<T, U>: 当且仅当T可以隐式转换为U时, 该 concept 才为真.std::common_reference_with<T, U>: 当且仅当T和U有一个公共的引用类型时, 该 concept 才为真.std::common_with<T, U>: 当且仅当T和U有一个公共的类型时, 该 concept 才为真.std::assignable_from<T, U>: 当且仅当T可以从U赋值时, 该 concept 才为真.std::swappable<T>: 当且仅当T可以交换时, 该 concept 才为真.
#include <concepts>
#include <iostream>
class Base {};
class Derived : public Base {};
template <typename L, typename R>
void same_as(L lhs, R rhs) {
if constexpr (std::same_as<L, R>) {
std::cout << "same type\n";
} else {
std::cout << "not same type\n";
}
}
template <typename D, typename B>
void derived_from(D d, B b) {
if constexpr (std::derived_from<D, B>) {
std::cout << "Derived from Base" << std::endl;
} else {
std::cout << "Not derived from Base" << std::endl;
}
}
template <typename L, typename R>
void convertible_to(L l, R r) {
if constexpr (std::convertible_to<L, R>) {
std::cout << "Convertible" << std::endl;
} else {
std::cout << "Not convertible" << std::endl;
}
}
template <typename L, typename R>
void common_reference_with(L& l, R& r) {
if constexpr (std::common_reference_with<L, R>) {
std::cout << "Common reference" << std::endl;
} else {
std::cout << "No common reference" << std::endl;
}
}
template <typename L, typename R>
void common_with(L& l, R& r) {
if constexpr (std::common_with<L, R>) {
std::cout << "Common" << std::endl;
} else {
std::cout << "Not common" << std::endl;
}
}
template <typename T, typename U>
void assignable_from(T& t, U& u) {
if constexpr (std::assignable_from<T&, U>) {
std::cout << "Assignable" << std::endl;
} else {
std::cout << "Not assignable" << std::endl;
}
}
template <typename T>
void swappable(T& t1, T& t2) {
if constexpr (std::swappable<T>) {
std::cout << "Swappable: " << t1 << " " << t2 << std::endl;
} else {
std::cout << "Not swappable" << std::endl;
}
}
int main() {
same_as(1, 3.14); // not same type
same_as(0, 1); // same type
Derived derived;
Base base;
derived_from(derived, base); // Derived from Base
derived_from(1, base); // Not derived from Base
convertible_to(1, 3.14); // Convertible
convertible_to(1, "1"); // Not convertible
int i = 1, j = 2;
double d = 3.14;
common_reference_with(i, d); // Common reference
common_reference_with(derived, base); // Common reference
common_reference_with(i, base); // No common reference
common_with(i, d); // Common
common_with(derived, base); // Common
common_with(i, base); // No common
assignable_from(i, j); // Assignable
assignable_from(derived, base); // Not assignable
assignable_from(base, derived); // Assignable
const int ci = 0;
assignable_from(ci, d); // Outputs: Not assignable
int l = 0, r = 1;
const int cl = 0, cr = 1;
swappable(l, r); // Swappable: 0, 1
swappable(cl, cr); // Not swappable
return 0;
}
数学 concepts
std::integral: 整数类型. 包含bool, char, char8_t, char16_t, char32_t, wchar_t, short, int, long, long long,
以及对应的有符号和无符号类型,以及带 const 修饰符的类型.std::signed_integral: 有符号整数类型, 满足std::integral且是带符号的std::unsigned_integral: 无符号整数类型std::floating_point: 浮点数类型
#include <concepts>
#include <iostream>
using namespace std;
void print(std::signed_integral auto value) {
std::cout << "Signed Integral: " << value << '\n';
}
void print(std::unsigned_integral auto value) {
std::cout << "Unsigned Integral: " << value << '\n';
}
void print(std::floating_point auto value) {
std::cout << "Floating Point: " << value << '\n';
}
int main() {
print(10);
print(-5);
print(20u);
print(3.14);
}
运行输出:
Signed Integral: 10
Signed Integral: -5
Unsigned Integral: 20
Floating Point: 3.14
生命周期 concepts
std::default_initializable: 默认初始化的std::copy_constructible: 可拷贝构造的std::move_constructible: 可移动构造的std::constructible_from: 可构造的std::destructible: 可销毁的
#include <concepts>
#include <iostream>
class Plain {};
class SingleInstance {
public:
SingleInstance() = default;
SingleInstance(const SingleInstance&) = delete;
SingleInstance(SingleInstance&&) = delete;
};
template <typename T>
class Container {
public:
void resize(size_t new_size)
requires std::default_initializable<T> && //
std::destructible<T> //
{}
void push_back(const T&)
requires std::copy_constructible<T> //
{}
void push_back(T&&)
requires std::move_constructible<T> //
{}
template <typename... Args>
void emplace_back(Args&&... args)
requires std::constructible_from<T, Args...> //
{}
};
int main() {
Container<Plain> a;
a.resize(10);
a.push_back(Plain());
a.emplace_back();
Container<SingleInstance> b;
b.resize(10); // OK
b.push_back(SingleInstance()); // ERROR, not copy constructible
b.emplace_back();
}
运行输出:
比较类的 concepts
-
std::equality_comparable: 相等性比较.是指结构体提供了等于==和!=. 为了保证代码的逻辑正确, 通常只提供==或者!=. 另外一个完全可以由编译器推导出来. -
std::totally_ordered: 完全有序. 是指提供了比较运算符<, <=, >, >=.
同样,为了保证逻辑上的一致性,
四个比较操作符中只需要实现一个(通常是<),
其他的关系完全可以推导出来.
只能用相等性而无法用有序性的场景, 如: C++中与nullptr的比较, 数学上的与无穷大的比较等.
常见的关联容器可以分为两类: 基于比较的和基于哈希的.
- 基于比较的比如
std::set, std::map, std::multiset, std::multimap等. 通常其底层实现是红黑树(二分查找树的一种). - 基于哈希的比如
std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap等.
通常其底层实现是哈希表. 哈希表的原理中需要计算键(Key)的哈希值,
所以键类型需要提供哈希函数. 在发生键冲突的时候, 为了保证键的唯一性,
还需要提供相等性比较函数来区别不同的键.
#include <concepts>
#include <cstdint>
#include <cstring>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
#include <unordered_set>
using namespace std;
struct Equality {
string data;
bool operator==(const Equality& rhs) const { //
return data == rhs.data;
}
bool operator!=(const Equality& rhs) const { return !(*this == rhs); }
bool operator<(const Equality&) const = delete;
bool operator<=(const Equality&) const = delete;
bool operator>(const Equality&) const = delete;
bool operator>=(const Equality&) const = delete;
};
struct Ordered {
int a = 0;
bool operator<(const Ordered& rhs) const { //
return a < rhs.a;
}
bool operator>(const Ordered& rhs) const { return rhs < *this; }
bool operator==(const Ordered& rhs) const {
return !(*this < rhs) && !(rhs < *this);
}
bool operator<=(const Ordered& rhs) const { return !(rhs < *this); }
bool operator>=(const Ordered& rhs) const { return !(*this < rhs); }
};
int main() {
auto hash_fun = [](Equality const& key) {
return std::hash<std::string>()(key.data);
};
std::map<Ordered, int> map;
std::unordered_map<Equality, int, decltype(hash_fun)> hashmap;
std::set<Ordered> set;
std::unordered_set<Equality, decltype(hash_fun)> hashset;
map<Equality, int> em; // Error
unordered_map<Ordered, int> eum; // Error
}
对象相关的 concepts
std::regular: 正则的, 是指一个对象可以被复制(copyable),默认构造(default constructible), 以及比较相等性(equality comparable).std::semiregular: 半正则的, 比正则少了比较相等性的要求.std::movable: 可移动的std::copyable: 可拷贝的
#include <concepts>
#include <iostream>
// copyable, default constructible, and equality comparable.
struct Regular {
int a = 0;
bool operator==(const Regular& rhs) const { return a == rhs.a; };
};
// copyable, default constructible, but not equality comparable.
struct SemiRegular {
int a = 0;
bool operator==(const SemiRegular&) const = delete;
};
template <std::semiregular T>
void isSemiregular() {
std::cout << typeid(T).name() << " is semi-regular\n";
}
template <std::regular T>
void isRegular() {
std::cout << typeid(T).name() << " is regular\n";
}
int main() {
isSemiregular<SemiRegular>();
isRegular<Regular>();
isSemiregular<Regular>();
}
运行输出:
struct SemiRegular is semi-regular
struct Regular is regular
struct Regular is semi-regular
可调用的 concepts
std::invocable: 可调用的std::regular_invocable: 可调用且不改变入参, 另外一点就是对相同的输入参数会得到相同的输出.std::predicate: 谓词, 返回值为 bool 类型
#include <concepts>
#include <iostream>
void print(int i) { std::cout << "Value: " << i << std::endl; }
bool is_even(int i) { return i % 2 == 0; }
template <std::invocable<int> Func>
void invocable_example(Func func) {
func(10);
}
template <std::regular_invocable<int> Func>
void regular_invocable_example(Func func) {
func(20);
}
template <std::predicate<int> Pred>
void predicate_example(Pred pred) {
if (pred(30)) {
std::cout << "Predicate returned true." << std::endl;
} else {
std::cout << "Predicate returned false." << std::endl;
}
}
int main() {
invocable_example(print);
regular_invocable_example(print);
predicate_example(is_even);
}
运行输出:
Value: 10
Value: 20
Predicate returned true.
工具类的库
std::input_iterator: 输入迭代器std::output_iterator: 输出迭代器std::forward_iterator: 前向迭代器std::bidirectional_iterator: 双向迭代器std::random_access_iterator: 随机访问迭代器std::contiguous_iterator: 连续迭代器
算法相关的 concepts
std::permutable: 可在原地排序, 不需要额外的内存.std::mergeable: 可以合并两个有序序列到输出序列.std::sortable: 可形成有序序列.
自定义requires表达式
C++20 支持自定义 Concepts, 目前有四种方法.
简单要求
requires表达式的语法为:
requires (parameter-list(optional)) {requirement-seq}
其中parameter-list是可选的, 用于指定requires表达式的参数. requirement-seq是一个或多个requirement的序列. 后文会详细介绍.
#include <string>
#include <vector>
template <typename T>
concept Arith = requires(T a, T b) { // simple requirement
a + b;
a - b;
a* b;
a / b;
};
int main() {
static_assert(Arith<int>); // OK
static_assert(Arith<double>); // OK
static_assert(Arith<std::string>); // Error
static_assert(Arith<std::vector<int>>); // Error
}
Concept Addable要求类型T支持+操作符.
类型要求
在类型要求中, 必须使用关键字typename和类型名.
#include <list>
#include <vector>
template <typename T>
struct Container {
using value_type = T;
};
template <typename T>
concept HasValueType = requires {
typename T::value_type; // type requirement
};
int main() {
static_assert(HasValueType<std::vector<int>>); // OK
static_assert(HasValueType<std::list<double>>); // OK
static_assert(HasValueType<Container<int>>); // OK
static_assert(HasValueType<int>); // Error
}
复合要求
复合要求的形式如下:
{expression} noexcept(optional) return-type-requirement(optional);
#include <concepts>
#include <iterator>
#include <list>
#include <vector>
template <typename T>
concept CompReq = requires(T t) { // compound requirement
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
};
int main() {
static_assert(CompReq<std::vector<int>>); // OK
static_assert(CompReq<std::list<double>>); // OK
static_assert(CompReq<int>); // Error
}
嵌套要求
#include <concepts>
#include <list>
#include <vector>
template <typename T>
concept Container = requires(T t) {
{ t.begin() } -> std::input_iterator;
{ t.end() } -> std::input_iterator;
};
template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
template <typename T>
concept NestedReq = requires(T) { // nested requirement
Container<T>;
requires Arithmetic<typename T::value_type>;
};
int main() {
static_assert(NestedReq<std::vector<int>>); // OK
static_assert(NestedReq<std::list<double>>); // OK
static_assert(NestedReq<int>); // Error
}
总结
- Concepts 通过对类型参数施加约束来提高模板的可读性和可维护性.
- Concepts 可以应用于 requires 语句,尾部 requires 语句,受约束的模板参数和函数模板缩写.
- Concepts 是可以用作编译时谓词.可以基于 Concepts 进行重载,在 Concepts 上特化模板,在成员函数或可变参数模板上使用 Concepts.
- 由于 C++20 和 Concepts,使用未约束的占位符(auto)和约束的占位符(Concepts)是统一的.
- 由于新的缩写函数模板语法,定义函数模板变得非常简单.
- 不要重复发明轮子.在定义自己的 Concepts 之前,请研究 C++20 标准中丰富的预定义 Concepts.

4275

被折叠的 条评论
为什么被折叠?



