跟我学C++中级篇——Concepts的循环依赖

一、循环依赖

循环依赖,这个东西非常可怕。如果是简单的依赖还好说,直接大家都可以看出来。但在复杂的场景下,如果产生这种循环依赖,很难发现,而且问题也可能比较隐藏,错误提示也不友好。

产生循环依赖的原因简单的说就是互相将彼此的任务完成当前自己任务完成的前提。从这个角度看,线程的死锁、类(头文件)、条件判断以及类似的场景都有可能产生循环依赖。不过相对于线程的死锁广泛流行为开发者熟知,条件判断可能太简单,基本很难有人写出这种低级错误,即使真写出来也很容易发现。

但是在C++编程中,模板编程相对于开发者们来说,本身就是一小众的场景,而对于概念这个C++20才推出的新特性可能更加用得少了。不过简单了解后会发现它其实也是一种条件约束机制,那么它有没有可能会发生循环依赖的情况呢?

二、Concepts的循环依赖

concepts产生循环依赖,一般是在有相当复杂度的模板编程设计中。如果不是特殊原因或者概念开发特别生疏的情况下,很难出现这种循环依赖。也正是因为产生的原因比较复杂,就越是让开发者难以解决,毕竟经验少么。

正常情况下的概念约束,不会复杂到约束太多个条件,但在一些模板库的开发中,可能由于设计的原因,导致多个模块中的概念线束产生依赖,特别是递推非线性循环依赖,非常隐蔽。另外比如模板参数中同时受多个概念的约束,而这些概念间又可能有互相约束的条件。

说得更直白一些,就好像一个人证明一个公式,这个公式的成立条件是A定理,A定理又依赖B定理,而B定理又依赖当前这个公式,这其实就是一种现实的"概念的循环依赖"。下面看一个简单的例子:

c 复制代码
template<typename T>
concept A = requires(T t) {
    requires B<T>;  // 依赖概念B
};

template<typename T>
concept B = requires(T t) {
    requires A<T>;  // 依赖概念A
};

三、如何解决

既然知道了概念有可能产生循环依赖,最简单的方法就是将概念本身约束在严格的范围内,不要动不动写出跨编译单元甚至模块的概念。这样除了可以尽量减少产生依赖的情况,即使真得出现依赖的情况,也容易发现和解决。但在一些场景下可能不可避免的会出现复杂的交互逻辑设计,那就需要设计要遵循下面的几种设计理念:

  1. 对复杂概念要进行更高层次的抽象
    将分层的设计引入到概念设计中,对同一类型的约束不要进行重复的多次概念约束,比如对同一个类的不同的属性约束,这时就可以抽象出一个更高的约束,统一进行管理
  2. 尽量减少概念的互相约束的可能性
    概念的约束尽量自耦合而与其约束解耦,也就是设计中的功能最小化原则
  3. 将复杂概念进行分解
    如果出现了复杂的概念,则首先考虑是不是可以将其分解成多个小的概念进行处理。如果无法拆解再考虑避免依赖

而在实际的开发中,如果已经出现了循环依赖的情况,除了利用上述的设计进行处理外,也可以使用一些编程的技巧的来处理:

  1. 前向声明
    前向声明是个宝。但需要知道的,Concepts本身并不支持前向声明,开发者只能通过对依赖的循环参数中的依赖通过前向声明打破这个依赖的链条,从而解决循环依赖。
c 复制代码
// A.h
#include "B.h"

template<typename T>
concept CheckB = requires(T t) {
    t.bFunc();
};

class A {
public:
    void testB(CheckB auto& b); //  concept约束
};
// B.h
#include "A.h"

class B {
public:
    A* testA();
    // ...
};

解决方法:

c 复制代码
// A.h
class B;  
class A {
public:
    void testB(class B& b);
};

// B.h
class A;
class B {
public:
    A* testA();
};

// concepts.h  单独进行鲁出
#include "B.h"
template<typename T>
concept CheckBfunc = requires(T t) {
    t.bFunc();
};

它其实就是普通的类依赖的例子,不过在模板编程中需要注意的前向声明有可能导致模板的ADL产生问题。

  1. 使用延迟加载处理约束

在概念中可以使用Requirs子句来将约束的检查推迟到实例化时再进行处理

c 复制代码
template<typename T>
concept A = requires {
    // 延迟到实例化
    requires requires(T t) { requires B<T>; };
};

template<typename T>
concept B = requires {
    requires requires(T t) { requires A<T>; };
};

例程整体的比较简单,主要是说明方法。因为目前还没有真正的实际的工程中开发出或遇到相关的Concepts循环依赖的代码。

四、总结

总体上来看,一般来说从设计上就可以避免绝大多数的概念的循环依赖。可见,代码的编写其实最终拼的还是设计思想和设计理念。编程技巧只是在理念和思想下的一种闪光点而不能决定编程高度的基础。

相关推荐
訫悦2 小时前
C++自带的set get语法(MSVC)
开发语言·c++
墨雪不会编程3 小时前
C++之【list详解篇一】如何玩好链表
c++·链表·list
柏木乃一3 小时前
Linux进程信号(2):信号产生part2
linux·运维·服务器·c++·信号处理·信号·异常
HAPPY酷3 小时前
C++ 成员指针(Pointer to Member)完全指南
java·c++·算法
Sunsets_Red3 小时前
浅谈随机化与模拟退火
java·c语言·c++·python·算法·c#·信息学竞赛
星火开发设计4 小时前
模板参数:类型参数与非类型参数的区别
java·开发语言·前端·数据库·c++·算法
忘梓.4 小时前
二叉搜索树·极速分拣篇」:用C++怒肝《双截棍》分拣算法,暴打节点删除Boss战!
开发语言·c++·算法
闻缺陷则喜何志丹4 小时前
【C++DFS 马拉车】3327. 判断 DFS 字符串是否是回文串|2454
c++·算法·深度优先·字符串·力扣·回文·马拉车
晨非辰4 小时前
【数据结构入坑指南(三.1)】--《面试必看:单链表与顺序表之争,读懂“不连续”之美背后的算法思想》
数据结构·c++·人工智能·深度学习·算法·机器学习·面试