当前位置: 首页 > article >正文

C++20中的概念(Concepts)到底是什么概念?

C++20中的概念(Concepts)到底是什么概念?

大家好!今天我们来聊聊C++20中的一个非常酷的新特性——概念(Concepts)。C++的模板编程功能非常强大而复杂,但也经常让人头疼,时常让我陷入自我怀疑:我真的懂C++么?我还敢在简历里面写精通C++么?😅😅😅

概念的引入可以说是为模板编程带来了福音,终于可以让我以轻松、愉悦的方式来进行模板编程了,今天就跟大家聊聊Concepts,带你了解C++20是如何让模板编程变得更简单易用的。
c++-concept

什么是概念(Concepts)?

先给大家一个简单的比喻。想象一下,我们在招募成员来组建一个乐队。我们当然希望招募的成员能够符合一些基本要求。比如,吉他手至少要会弹吉他,鼓手至少要会打鼓。这些基本要求就是我们所说的概念(Concepts)。

在C++中,Concepts 就是用来描述模板参数应该满足的一些条件和约束。这些条件通常在模板函数或类中显式规定,以确保传入的参数类型符合预期。

requires表达式

requires表达式是C++20中引入的一种新机制,用来在模板的上下文中进行约束和检查。通过requires表达式,你可以明确地规定模板参数需要满足的条件,从而使代码更具可读性和表达力。

requires表达式用于指定类型必须满足的条件。其基本语法如下:

template<typename T>
concept ConceptName = requires (T a, T b) {
    // 要求表达式
    { a + b } -> std::convertible_to<T>;
    // 其他更多的条件...
};

在上面的语法中,requires后面跟着一个带参数的表达式块,里面列出了模板参数需要满足的条件。

requires表达式不仅可以用来检查操作符,还可以用来验证类型特征(type traits)、函数存在性、成员变量等。你可以使用requires表达式来构建非常复杂的概念。

例子:检查类型是否有某个成员函数

template<typename T>
concept HasSizeFunction = requires(T t) {
  { t.size() } -> std::convertible_to<std::size_t>;
};

这个概念 HasSizeFunction 要求类型 T 必须有一个 size() 成员函数,并且 size() 的返回值必须可以转换为 std::size_t

基本用法

让我们通过一些代码实例来看看Concepts是如何使用的。

常见案例:检查类型是否支持 + 运算符

假设我们有一个简单的模板函数需要两个参数相加。在C++20之前,我们也许会这样写:

template<typename T>
T add(T a, T b) {
  return a + b;
}

看似简单,但如果你传入的是不支持加法操作的类型,这段代码就会在你编译的时候爆出一堆复杂的错误信息,让人摸不着头脑。

在C++20中,我们可以用Concepts来明确限定模板参数必须支持加法操作。看例子:

#include <concepts>
#include <iostream>

// 定义一个概念来描述需要支持加法操作的类型
template<typename T>
concept Addable = requires(T a, T b) {
  { a + b } -> std::convertible_to<T>;
};

template<Addable T>
T add(T a, T b) {
  return a + b;
}

int main() {
  std::cout << add(1, 2) << std::endl;         // 正常
  std::cout << add(1.5, 2.5) << std::endl;     // 正常
  // std::cout << add("a", "b") << std::endl; // 编译报错,因为字符串不支持 + 运算
  return 0;
}

在这个例子中:

  1. requires(T a, T b):定义了两个用于测试的变量 ab
  2. { a + b } -> std::convertible_to<T>;:表示类型T必须支持+操作,并且a + b的结果类型必须可以转换为T。

Addable 是一个概念,它规定了类型 T 必须支持 a + b 运算,而且结果类型必须可以转换为T。然后我们在模板函数 add 中使用这个概念来约束模板参数。

进阶案例:检查类型是否是迭代器

概念不仅可以让代码更易读,还能提高编译时的错误信息质量,使其更明确、更有指导性。让我们再看看一个例子,这次我们检查类型是否支持迭代。

template<typename T>
concept Iterable = requires(T t) {
  { t.begin() } -> std::input_iterator;
  { t.end() } -> std::input_iterator;
};

template<Iterable T>
void print_all(const T& container) {
  for (const auto& item : container) {
    std::cout << item << " ";
  }
  std::cout << std::endl;
}

int main() {
  std::vector<int> vec{1, 2, 3};
  print_all(vec); // 正常
  
  // int not_iterable = 5;
  // print_all(not_iterable); // 编译报错,因为int不是可迭代类型
  
  return 0;
}

在这个例子中:

  1. requires(T t):定义了一个用于测试的变量 t
  2. { t.begin() } -> std::input_iterator;:表示类型 T 必须有 begin() 方法,并且 begin() 的返回值类型必须是 std::input_iterator
  3. { t.end() } -> std::input_iterator;:表示类型 T 必须有 end() 方法,并且 end() 的返回值类型必须是 std::input_iterator

Iterable 概念定义了类型T需要有 begin()end() 函数,并且返回值需要是 std::input_iterator。然后我们在 print_all 函数中使用这个概念来约束模板参数。这样,如果传入一个不是可迭代的类型,编译器会立即报错。

在C++20之前的替代方案

在C++20之前,我们没有Concepts这个工具,因此我们常用其它方法来实现类似功能,如SFINAE(Substitution Failure Is Not An Error,多重替代失败并非错误)和类型特征(type traits)。这些方法可能会更复杂,更难读懂。我们来看几个例子:

示例:检查类型是否支持 + 运算符

在C++20之前,我们可以用SFINAE和类型特征来做到这一点:

#include <iostream>
#include <type_traits>

// 辅助函数检查 T 是否支持 + 操作符
template<typename T, typename = decltype(std::declval<T>() + std::declval<T>())>
std::true_type is_addable_impl(int);

template<typename>
std::false_type is_addable_impl(...);

template<typename T>
using is_addable = decltype(is_addable_impl<T>(0));

// 使用 SFINAE 的加法模板函数
template<typename T>
typename std::enable_if<is_addable<T>::value, T>::type add(T a, T b) {
  return a + b;
}

int main() {
  std::cout << add(1, 2) << std::endl;         // 正常
  std::cout << add(1.5, 2.5) << std::endl;     // 正常
  // std::cout << add("a", "b") << std::endl; // 编译报错,因为字符串不支持 + 运算
  return 0;
}

这里我们使用了SFINAE和std::enable_if来检查 T 类型是否支持 + 运算符。is_addable 特征确保 T 类型支持加法,然后在模板函数 add 中使用 enable_if 来使函数仅在 T 可加时启用。

示例:检查类型是否为可迭代容器

在C++20之前,我们也可以用SFINAE和类型特征来做到这一点:

#include <iostream>
#include <vector>
#include <type_traits>

// 辅助结构判断T是否有begin()和end()方法
template<typename T, typename = void>
struct is_iterable : std::false_type {};

template<typename T>
struct is_iterable<T, std::void_t<
    decltype(std::declval<T>().begin()),
    decltype(std::declval<T>().end())>> : std::true_type {};

// 使用 SFINAE 的打印函数
template<typename T>
typename std::enable_if<is_iterable<T>::value, void>::type
print_all(const T& container) {
  for (const auto& item : container) {
    std::cout << item << " ";
  }
  std::cout << std::endl;
}

int main() {
  std::vector<int> vec{1, 2, 3};
  print_all(vec); // 正常
  
  // int not_iterable = 5;
  // print_all(not_iterable); // 编译报错,因为 int 不是可迭代类型
  
  return 0;
}

在这个例子中,我们使用 std::void_t 和 SFINAE 创建了 is_iterable 特征,用以检查类型 T 是否有 begin()end() 方法。然后在 print_all 函数模板中使用 enable_if 来限定 T 必须是可迭代的类型。

到了这里,不知道真正读懂上面代码的小伙伴有几个?是不是真真切切体会到了Concepts给C++编程带来的简化程度,简直就是一个飞越!

预定义概念(Predefined Concepts)

那么除了我们自己亲自动手定义的Concepts,是不是有一些预定义的Concepts呢?答案是肯定的。C++20 自带了很多常用的概念,这些概念能大大简化我们的代码,让你写模板代码不再抓耳挠腮。

常见的预定义概念

  1. Same:检查两个类型是否相同。

    #include <concepts>
    
    template<typename T, typename U>
    concept Same = std::is_same_v<T, U>;
    
    Same<int, int> sameType; // 没问题
    Same<int, double> differentType; // 编译错误
    
  2. Integral:检查类型是否是整数类型。

    #include <concepts>
    
    template<std::integral T>
    T add(T a, T b) {
      return a + b;
    }
    
    add(1, 2); // 没问题
    add(1.0, 2.0); // 编译错误
    
  3. SignedIntegralUnsignedIntegral:检查类型是否是有符号整数或无符号整数。

    #include <concepts>
    
    template<std::signed_integral T>
    T subtract(T a, T b) {
      return a - b;
    }
    
    subtract(3, 1); // 没问题
    subtract(3U, 1U); // 编译错误
    
  4. FloatingPoint:检查类型是否是浮点数类型。

    #include <concepts>
    
    template<std::floating_point T>
    T multiply(T a, T b) {
      return a * b;
    }
    
    multiply(1.0, 2.0); // 没问题
    multiply(1, 2); // 编译错误
    
  5. DefaultConstructible:检查类型是否可以默认构造。

    #include <concepts>
    
    template<std::default_constructible T>
    T create() {
      return T();
    }
    
    create<int>(); // 没问题
    create<std::mutex>(); // 编译错误
    
  6. ConstructibleFrom:检查类型是否可以从给定的参数构造。

    #include <concepts>
    
    template<typename T, typename... Args>
    concept ConstructibleFrom = std::constructible_from<T, Args...>;
    
    static_assert(ConstructibleFrom<std::string, const char*>); // 没问题
    static_assert(ConstructibleFrom<std::string, int>); // 编译错误
    
  7. Destructible:检查类型是否可销毁。

    #include <concepts>
    
    template<std::destructible T>
    void destroy(T* ptr) {
      delete ptr;
    }
    
    destroy(new int(5)); // 没问题
    
  8. CopyableMovable:检查类型是否可复制或可移动。

    #include <concepts>
    
    template<std::copyable T>
    T copy(T a) {
      return a;
    }
    
    copy(5); // 没问题
    // unique_ptr<int> 不可复制,编译会报错
    
    template<std::movable T>
    void move(T&& a) {
       T b = std::move(a);
    }
    
    move(std::unique_ptr<int>(new int(5))); // 没问题
    

这些预定义概念很大程度上简化了我们写模板代码的过程,还让代码更加易读。

更好的编译错误

C++20 的概念不仅让代码更简洁,还大大提升了编译期错误提示的质量。来看一下对比。

没有概念时的模板错误

我们来看一个简单的模板函数,负责相加两个元素:

template<typename T>
T add(T a, T b) {
  return a + b;
}

int main() {
  add(1, 2); // 没问题
  add("Hello", "World"); // 编译错误
}

当你尝试编译这段代码时,如果传入不支持加法操作的类型,错误信息会比较模糊,而且很难理解。比如:

error: invalid operands of types ‘const char [6]’ and ‘const char [6]’ to binary ‘operator+’
      return a + b;
             ~ ^ ~

这个错误信息不够直观,尤其对于不熟悉细节的人来说。

使用概念后的模板错误

用概念,我们可以明确地限定模板参数:

#include <concepts>
#include <iostream>

template<typename T>
concept Addable = requires(T a, T b) {
  { a + b } -> std::convertible_to<T>;
};

template<Addable T>
T add(T a, T b) {
  return a + b;
}

int main() {
  std::cout << add(1, 2) << std::endl;         // 没问题
  std::cout << add(1.5, 2.5) << std::endl;     // 没问题
  // std::cout << add("Hello", "World") << std::endl; // 编译错误
  return 0;
}

编译时,如果类型不符合要求(比如传入的是const char*),错误信息会更清楚:

error: constraints not satisfied for function template 'T add(T, T)'
  3 | template<Addable T>
       ^
  required for the satisfaction of 'Addable<T>' [with T = const char*]
  add.cpp: In instantiation of 'T add(T, T) [with T = const char*]':
  add.cpp:11:27:   required from here
  add.cpp:6:14: note: the expression 'a + b' is not convertible to 'const char*'
    6 |   { a + b } -> std::convertible_to<T>;

这个错误信息非常清晰:

  • 告诉你概念Addable<T>的约束条件不满足。
  • 明确指出不满足的条件是a + b的结果不能转换为T。

小结

quote-c-is-designed-to-allow-you-to-express-ideas-but-if-you-don-t-have-ideas-or-don-t-have-bjarne-stroustrup

C++20中的Concepts为模板编程带来了巨大的提升,让模板代码变得更加清晰、易读且易于维护。通过Concepts,我们可以明确指定模板参数应满足的要求,避免了在编译时因为不匹配的类型而报出一堆晦涩难懂的错误信息。

希望这个讲解能够让你对C++20中的Concepts有一个直观的认识。如果大家有任何问题或对Concepts有更多的好奇,欢迎在评论区留言讨论。感谢大家的阅读,我们下次再见!


http://www.kler.cn/a/398392.html

相关文章:

  • 【Chapter 3】Machine Learning Classification Case_Prediction of diabetes-XGBoost
  • WP网站如何增加文章/页面的自定义模板
  • 微信小程序02-页面制作
  • mysql数据迁移PolarDB
  • 网络安全练习之 ctfshow_web
  • H3C NX30Pro刷机教程-2024-11-16
  • Android - Pixel 6a 手机OS 由 Android 15 降级到 Android 14 操作记录
  • 六:从五种架构风格推导出HTTP的REST架构
  • 2024.5 AAAiGLaM:通过邻域分区和生成子图编码对领域知识图谱对齐的大型语言模型进行微调
  • 深度学习神经网络创新点方向(具体)
  • Linux——环境基础开发工具使用1
  • React Native 全栈开发实战班 - 原生功能集成之地理位置服务
  • 常用的Anaconda Prompt命令行指令
  • 【第三课】Rust变量与数据类型(二)
  • java.sql.SQLException Parameter index out of range
  • Ubuntu下的Eigen库的安装及基本使用教程
  • 14.最长公共前缀
  • 基于Kafka2.1解读Consumer原理
  • memblock内存分配器
  • 智能算法助力衍生品市场投资分析:正大科技的量化模型应用
  • java组件安全
  • Scala入门基础(17.1)Set集习题
  • Kotlin深度面试题:协程、密封类和高阶函数
  • 计算机网络 (3)计算机网络的性能
  • React前端框架入门教程:从零开始构建一个简单的任务管理应用
  • 麒麟Server下安装东方通TongLINK/Q