您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 
 订阅
C++20详解:Concept
 
 
   次浏览      
 2024-9-11
 
编辑推荐:
本文主要介绍了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;
}

 

但是这样也会带来新的麻烦,那就是可能会有一些意料之外的情况出现,比如:

add(true, false);

当编译器看到这一语句时,虽然能够进行模板实例化:

#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 的细分。

   
次浏览       
相关文章

深度解析:清理烂代码
如何编写出拥抱变化的代码
重构-使代码更简洁优美
团队项目开发"编码规范"系列文章
相关文档

重构-改善既有代码的设计
软件重构v2
代码整洁之道
高质量编程规范
相关课程

基于HTML5客户端、Web端的应用开发
HTML 5+CSS 开发
嵌入式C高质量编程
C++高级编程

最新活动计划
C++高级编程 12-25 [线上]
白盒测试技术与工具实践 12-24[线上]
LLM大模型应用与项目构建 12-26[特惠]
需求分析最佳实践与沙盘演练 1-6[线上]
SysML建模专家 1-16[北京]
UAF架构体系与实践 1-22[北京]
 
 
最新文章
.NET Core 3.0 正式公布:新特性详细解读
.NET Core部署中你不了解的框架依赖与独立部署
C# event线程安全
简析 .NET Core 构成体系
C#技术漫谈之垃圾回收机制(GC)
最新课程
.Net应用开发
C#高级开发技术
.NET 架构设计与调试优化
ASP.NET Core Web 开发
ASP.Net MVC框架原理与应用开发
成功案例
航天科工集团子公司 DotNet企业级应用设计与开发
日照港集 .NET Framewor
神华信 .NET单元测试
台达电子 .NET程序设计与开发
神华信息 .NET单元测试