C++20 Concepts简介

简介

在使用 STL 库的时候, 我们经常会遇到编译器的错误提示冗长且难以理解的情况. 这是因为编译器对模板的编译是分为两步, 第一步是模板的实例化, 使用用户提供的类型去替代模板参数, 第二步是对实例化后的代码进行编译. 编译器报告错误的时候往往是在第二步.

C++20 引入了 Concepts, 它是一种对模板进行约束的机制. Concept 可以用在函数模板(Function Template), 类模板(Class Template), 通用函数成员(Generic Member Function)上. 能对模板参数, 函数参数进行约束. 约束作为接口的一部分, 允许编译器对其进行检查, 能让编译器更早发现错误, 提供更好的错误信息.

如何使用 Concepts

有四种方式使用concepts:

  1. requires语句
  2. 尾部的 requires 语句
  3. 受约束的模板参数
  4. 函数模板缩写
#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

#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

#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

#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

只能用相等性而无法用有序性的场景, 如: C++中与nullptr的比较, 数学上的与无穷大的比较等.

常见的关联容器可以分为两类: 基于比较的和基于哈希的.

#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

#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

#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.

工具类的库

算法相关的 concepts

自定义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
}

总结