C++20 Concepts简介

C++20 Concepts简介

简介

在使用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>
当且仅当 TU是相同的类型时, 该concept才为真.
std::derived_from<T, U>
当且仅当 TU的派生类时, 该concept才为真.
std::convertible_to<T, U>
当且仅当 T可以隐式转换为 U时, 该concept才为真.
std::common_reference_with<T, U>
当且仅当 TU有一个公共的引用类型时, 该concept才为真.
std::common_with<T, U>
当且仅当 TU有一个公共的类型时, 该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的比较,Python中与None{.python}的比较等,数学上的与无穷大的比较等.

常见的关联容器可以分为两类: 基于比较的和基于哈希的.
基于比较的比如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.

后记

笔者的原始文档为Latex格式, 转为makrdown格式之后丢失了许多有用的样式, 如代码引用和代码高亮. 因此我上传了PDF格式的文档, 有兴趣的朋友可以下载查看.
样例1
样例2

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值