在一次开发过程中,我遇到了一个循环依赖的问题。一方面,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博客)