记在一次C++循环依赖问题中偶然发现的解决方法

在一次开发过程中,我遇到了一个循环依赖的问题。一方面,log.h中的类实现日志系统,需要线程系统提供线程安全(Mutex类属于thread.h);另一方面,thread.h中的类实现的线程系统同样需要日志系统输出日志(Logger类属于log.h)。于是两个头文件相互包含,名称空间也彼此联通。

//thread.h
#pragma once
#include "log.h"
namespace ThreadSpace
{
    using namespace LogSpace;    //报错:“LogSpace不是名称空间名”    
}

//thread.cpp
#include "thread.h"
#include "log.h"
namespace ThreadSpace
{
    Logger logger;
}

//log.h
#pragma once
#include "thread.h"
namespace LogSpace
{
    using namespace ThreadSpace;
    class Logger
    {
    private:
        Mutex m;    //报错:“Mutex不是类型”
    }
}

然而,这会导致头文件循环依赖的问题(尽管已经加入了#pragma once)。在这种情况下,由于头文件相互包含,编译时会出现有一方不认得另一方的内容的情况。在我的这个例子中,log.h表示“Mutex不是类型”,而thread.h则表示“LogSpace不是名称空间名”。

其中关于不认识名称空间的问题,可以用经典的前置声明的方法来解决:

//thread.h
#pragma once
#include "log.h"
namespace LogSpace{}       //前置声明
namespace ThreadSpace
{
    using namespace LogSpace;    //无报错
}

//thread.cpp
#include "thread.h"
#include "log.h"
namespace ThreadSpace
{
    Logger logger; 
}

//log.h
#pragma once
#include "thread.h"
namespace ThreadSpace{}   //前置声明
namespace LogSpace
{
    using namespace ThreadSpace;
    class Logger
    {
    private:
        Mutex m;    //报错:“Mutex不是类型”
    }
}

在有了前置声明以后,两个头文件都事先认识了对方头文件中的名称空间,thread.h也不会表示“LogSpace不是名称空间名”。然而,前置声明用于解决类的问题有一定的局限性:

//thread.h
#pragma once
#include "log.h"
namespace LogSpace{}
namespace ThreadSpace
{
    using namespace LogSpace;
}

//thread.cpp
#include "thread.h"
namespace ThreadSpace
{
    Logger logger;
}

//log.h
#pragma once
#include "thread.h"
namespace ThreadSpace{}
namespace LogSpace
{
    using namespace ThreadSpace;
    class Mutex;
    class Logger
    {
    private:
        Mutex m;    //报错:“不允许使用不完整的类型”
    } 
}

尽管在log.h中有了Mutex类型的前置声明,log.h认识了Mutex类,然而用Mutex类型来定义变量却会报错“不允许使用不完整的类型”,这是由于对于类而言前置声明没有类的定义(Mutex类定义在源文件里面,log.h还不认识),因此类是不完整的,Mutex类只能用于定义指针、引用、或者用于函数形参的指针和引用,不能用来定义对象,或访问类的成员。

这是因为需要确定Logger类空间占用的大小,但Mutex类还没有定义不能确定大小,而Mutex类的指针类型大小已知,因此Logger类中可以定义Mutex类的指针成员变量。

然而,在这次遭遇中,我偶然发现了另一个可以直接定义Mutex类(而不是指针)解决方法。由于只有thread.cpp中需要调用日志系统,而thread.h本身并不需要,所以可以令thread.h不包含log.h,避免了循环依赖,而是改为由thread.cpp包含log.h:

//thread.h
#pragma once
namespace LogSpace{}
namespace ThreadSpace
{
    using namespace LogSpace;    //不报错
}


//thread.cpp
#include "thread.h"
#include "log.h"
namespace ThreadSpace
{
    Logger logger;    //不报错
}

//log.h
#pragma once
#include "thread.h"
namespace ThreadSpace{}
namespace LogSpace
{
    using namespace ThreadSpace;
    class Mutex;
    Mutex m;        //不报错
}

由于thread.cpp同时包含log.h和thread.h,所以实际上预处理器会先处理"log.h",然后处理thread.h, 所以thread.h中的代码可以看到"log.h"中定义的内容。

然而,这是一种及其巧合的情况,虽然结果是解决了问题,但可读性大大降低且缺乏可复现性。并且,在这种情况下前文提到的名称空间仍需前置声明。

在迫不得已的情况下,还是应该采用依赖倒置(创建一个中间的抽象层)的方法来解决。这种解决方法比较地道,但也存在一定的缺点。

可见循环依赖是一个棘手的问题,最好的解决方法还是避免两个头文件的相互包含,从根本上解决问题才是上策。

( 部分内容参考:c++基础-头文件相互引用与循环依赖问题_c++头文件相互引用-CSDN博客

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值