模块化C++设计

Modular C++ Design

本文关键字:设计 C++ 模块化      更新时间:2023-10-16

我正在设计一个包含多个模块的工具包。我正在努力使模块尽可能独立,这样它们甚至可以独立编译(例如作为库)。

其中一个模块是logging,另一个模块则是geometry。现在,geometry中的一个基类接收到一个指向logging对象的指针,然后使用它来记录数据:

#include "../logging/logger.h"
class GeometryBase {
public:
//...
void do_something() { if (logger) logger->debug("doing something"); }
void setLogger(Logger* logger) {//...};
private:
Logger* logger = nullptr;
};

因此,我需要包含../logging/logger.h,这意味着编译这个模块需要logging头。有没有办法绕过这一点,即使logging标头不存在,它仍然可以编译?

现在我可以考虑在预处理过程中使用宏使所有与日志记录相关的部分都有条件。类似:

#ifdef USE_LOGGING
#include "../logging/logger.h"
#endif
class GerometryBase {
//...
void do_something() { if (logger) _log("doing something"); }
#ifdef USE_LOGGING
void _log(const std::string& s) {//...}
Logger* logger = nullptr;
#else
void _log(const std::string& s) {// do nothing}
void* logger = nullptr;
#endif
}; // class

有更好/更干净的方法吗?此类设计是否有推荐的指导方针或最佳实践?

=================================================

更新

下面是一个使用函数指针(基于rioki的想法)的示例实现,它确实有助于解耦对象:

目标h

#ifndef MYOBJ_H_
#define MYOBJ_H_
#include <iostream>
class MyObj {
public:
MyObj() { std::cout << "constructing MyObj" << std::endl;  }
void setLogger( void (*p)(const char*, int) ) {
logger = p;
}
void do_somthing() {
if (logger) {
logger("this is a debug message", 1);
}
} 
private:
void (*logger)(const char*, int ) = nullptr;
};
#endif 

logger.h

#ifndef LOGGER_H
#define LOGGER_H
void logger(const char* , int);
#endif

记录器.cpp

#include <iostream>
#include "logger.h"
void logger(const char* str, int lvl) {
std::cout << "level " << lvl << " " << str << std::endl;
}

main.cpp

#include "logger.h"
#include "obj.h"
int main() {
MyObj obj;
obj.setLogger(logger);
obj.do_somthing();

return 0;
}

输出:

constructing MyObj
level 1 this is a debug message

对于"这样它们甚至可以独立编译",您可以将类声明为一个类,

class Logger;

然后,您可以随意将它用于正式的结果和参数类型,但由于编译器不知道它的大小或它的成员,因此您无法使用它,例如在函数实现中。

但是,在另一个头中包含一个头和仅在实现文件中包含它之间有很大的区别:后者对总构建时间贡献一次,而前者可能贡献多次,每个翻译单元贡献一次。

另一方面,如果您只做头模块,那么就无法包含所有相关的代码。

您的几何模块中真的需要记录器吗?总是问"我真的需要B中的A吗?"来确定两个模块的耦合是否合理。

有多种方法可以消除两个模块之间的依赖关系。

几何类真的需要记录器吗?不,它只记录致命错误

然后抛出一个异常,以防出现致命错误,捕获它并将其记录在更高级别的代码中。这使得几何结构完全独立于记录器或任何其他模块。

几何类真的需要记录器吗?也许,我写了一堆诊断信息

不如为记录器定义一个完全虚拟的接口(抽象基类)。这只会引入对标头的依赖关系。您只需要接口的头,而不需要整个模块。如果指向记录器的指针为NULL,则不要记录任何内容。

您可以定义任何使用ostream编写诊断信息的函数吗。像这样,您可以捕获所有信息并将其记录到更高级别。这允许您传递字符串流或cout,并增加您的灵活性。您已经拥有的唯一依赖项是C++标准库。

不如将setLogger定义为不接受对象,而是接受std::function。例如:

class GerometryBase
{
public:
void setLogger(std::function<void (const std::string&)> value)
{
logger = value;
}
private:
std::function<void (const std::string&)> logger;
void log(const std::string& msg)
{
if (logger) 
{
logger(msg);
}
}
}

将记录器绑定到几何类:

Logger logger;
Box box;
box.setLogger([&] (const std::string& msg) {
logger.log(msg);
});

有很多方法可以减少模块之间的耦合。你只需要考虑一下。浏览标准库是我最喜欢的方式,这是有充分理由的。自从C++11引入lambdas以来,我的模块中的耦合显著减少。

您可以在公共头文件中声明接口,并在运行时解析具体的依赖关系。在您的示例中,几何模块包括定义抽象类Logger#include "common/logger.hpp"。几何体库的用户可以决定是使用记录器库中的记录器实现,还是实现自己的记录器实现。