【问题标题】:How can I handle circular #include calls in a template class header?如何处理模板类头中的循环#include 调用?
【发布时间】:2019-04-18 18:25:54
【问题描述】:

Error "Unterminated conditional directive" in cross-referencing headers相关

我有一个可序列化的模板类:

serializable.h

#pragma once
#ifndef SERIALIZABLE_H
#define SERIALIZABLE_H
#include "Logger.h"
#include <string>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/exception_ptr.hpp>

template<class T>
class Serializable {
public:
    static bool Deserialize(Serializable<T>* object, std::string serializedObject) {
        try {
            return object->SetValuesFromPropertyTree(GetPropertyTreeFromJsonString(serialized));
        } catch (...) {
            std::string message = boost::current_exception_diagnostic_information();
            Logger::PostLogMessageSimple(LogMessage::ERROR, message);
            std::cerr << message << std::endl;
        }
    }
private:
    static boost::property_tree::ptree GetPropertyTreeFromJsonString(const std::string & jsonStr) {
        std::istringstream iss(jsonStr);
        boost::property_tree::ptree pt;
        boost::property_tree::read_json(iss, pt);
        return pt;
    }
}
#endif // SERIALIZABLE_H

但问题是 Logger 类使用了一个从 Serializable 继承的 LogMessage 对象(使用 CRTP)。

Logger.h

#pragma once
#ifndef LOGGER_H
#define LOGGER_H
#include "LogMessage.h"

class Logger {
public:
    static void PostLogMessageSimple(LogMessage::Severity severity, const std::string & message);
}
#endif // LOGGER_H

LogMessage.h

#pragma once
#ifndef LOGMESSAGE_H
#define LOGMESSAGE_H
#include "serializable.h"

class LogMessage : public Serializable<LogMessage> {
public:
    enum Severity {
        DEBUG,
        INFO,
        WARN,
        ERROR
    };
private:
    std::string m_timestamp;
    std::string m_message;

    friend class Serializable<LogMessage>;
    virtual boost::property_tree::ptree GetNewPropertyTree() const;
    virtual bool SetValuesFromPropertyTree(const boost::property_tree::ptree & pt);
}

#endif // LOGMESSAGE_H

这里的问题是这些文件中的每一个都包含另一个导致构建错误的文件。不幸的是,我不能使用上述链接问题的解决方案(将#include“Logger.h”移动到Serializable.cpp),因为Serializable是一个模板类,因此需要在头文件中定义。

我不知道如何继续。任何帮助表示赞赏!

编辑: 我也考虑过在 serializable.h 中使用 Logger 和 LogMessage 的前向声明,但是因为我在 Logger 中调用静态方法并使用 LogMessage::Severity,所以这不起作用。

【问题讨论】:

  • 据我所知,你不能。只是不要这样写标题。例如将它们合并为一个。
  • 合并标头会违反关注点分离,并且在大型项目(就是这样)中根本无法很好地扩展
  • 写出(不在代码中)类之间的预期关系可能有助于揭示代码中不必要的依赖关系。例如:为什么Severity 枚举属于LogMessage 而不是Logger?为什么LoggerLogMessage 在同一个标​​题中?相关性:在提供的代码中(我假设已经简化),SerializableLogger 需要定义 LogMessage 的唯一原因是可以访问 Severity 枚举,这似乎是需要的弱理由类定义。
  • @JaMit,您说得对,代码已被简化以显示问题。实际上,LogMessage 和 Logger 位于不同的标头中。但是,为了不提供太多不必要的信息,我省略了。同样,Logger 严重依赖 LogMessage 的其他特性,确实需要包含它。
  • @KevinRak 我无意在 cmets 中回答,但由于我的问题可能会导致解决方案,因此我继续将一些相关想法收集到官方答案中。 (有点长。抱歉。)

标签: c++ templates circular-dependency template-classes


【解决方案1】:

有时循环依赖需要对所涉及的组件进行一些分析。弄清楚为什么圆存在,然后弄清楚为什么它不需要存在。分析可以在多个级别进行。这是我要开始的两个级别。

(由于代码显然是从真实代码中简化的,我会避免假设它显示了真正问题的严重程度。说到这里,感谢您没有用过多的细节淹没我们!代码足以概括地说明问题。我的回答中的任何具体建议同样是为了说明问题,不一定是最终的解决方案。)


在一个层面上,您可以查看课程的意图。忘记代码并专注于目标。 A 类在不知道 B 类是什么的情况下无法定义自己是否有意义?请记住,这比知道 B 类存在(这等同于前向定义,不需要标头)要强。如果不看代码就没有意义,那么也许你找到了可以做的事情。诚然,模板的使用使事情变得复杂,因为整个实现需要在标题中。

例如,Serializable 确实应该能够在不知道序列化将做什么的情况下定义自己(即Logger)。但是,它是一个模板,它的实现需要能够记录错误。所以……很棘手。

不过,这是一个可以寻找解决方案的地方。一种可能性可能是将错误日志记录到一个只处理字符串(已经序列化的数据)的基础部分和一个可以将LogMessage 转换为基础部分的字符串的翻译层。反序列化期间的错误强烈表明缺少任何要序列化的内容,因此日志记录可以直接进入基础部分。依赖圈会分解成链条:

Serializable -> LoggerBase
Logger -> LoggerBase
Logger -> LogMessage -> Serializable -> LoggerBase

在另一个层面上,您可以详细查看代码,而不必过多担心用途。你有标题 A 包括标题 B - 为什么? A 的哪些部分实际上使用了 B 的东西?实际使用了 B 的哪些部分?如果您需要更好地可视化这种依赖关系的位置,请绘制图表。然后考虑一些目的。这些部分是否定义在适当的位置?还有其他可能吗?

例如,在示例代码中,Serializable 需要LogMessage 的原因是为了获得对枚举LogMessage::ERROR 的访问权限。这不是需要整个 LogMessage 定义的充分理由。也许像PostLogErrorSimple 这样的包装器可以消除了解严重性常量的需要?也许现实比这更复杂,但关键是可以通过将依赖项推送到源文件中来回避某些依赖项。有时源文件用于不同的类。

另一个例子来自Logger 类。此类需要LogMessage 才能访问LogMessage::Severity 枚举(即具有ERROR 作为其值之一的枚举)。这也不是需要整个类定义的一个强有力的理由。也许枚举应该在别处定义?也许是Logger 的一部分?或者可能根本不在类定义中?如果这种方法有效,则依赖圈会分解成链:

Serializable -> Severity
Serializable -> Logger -> Severity // To get the PostLogMessageSimple function
Logger -> Severity

理想情况下,一旦处理好枚举,Logger 标头就可以通过 LogMessage 的前向声明来解决,而不是包含完整的标头。 (前向声明足以通过引用接收对象。大概完整的Logger 定义将有一些函数采用LogMessage 参数。)

【讨论】:

  • 如果有一些参考资料(例如你的博客)就更好了。我对你的文章很上瘾(所以回答)。 XD
【解决方案2】:

如果你只是在你的类中有声明并定义了方法,你应该能够使它工作:

#pragma once
#ifndef SERIALIZABLE_H
#define SERIALIZABLE_H
#include <string>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/exception_ptr.hpp>

template<class T>
class Serializable {
public:
    static bool Deserialize(Serializable<T>* object, std::string serializedObject);
private:
    static boost::property_tree::ptree GetPropertyTreeFromJsonString(const std::string & jsonStr);
}

#include "Logger.h"

template < typename T >
inline bool Serializable<T>::Deserialize(Serializable<T>* object, std::string serializedObject) {
        try {
            return object->SetValuesFromPropertyTree(GetPropertyTreeFromJsonString(serialized));
        } catch (...) {
            std::string message = boost::current_exception_diagnostic_information();
            Logger::PostLogMessageSimple(LogMessage::ERROR, message);
            std::cerr << message << std::endl;
        }
    }

template < typename T >
inline boost::property_tree::ptree Serializable<T>::GetPropertyTreeFromJsonString(const std::string & jsonStr) {
        std::istringstream iss(jsonStr);
        boost::property_tree::ptree pt;
        boost::property_tree::read_json(iss, pt);
        return pt;
    }

#endif // SERIALIZABLE_H

这还有一个额外的好处,就是让你的类界面更清晰。

【讨论】:

  • 假设源文件 (logger.cpp?) 以包含 logger.h 开头。 logger.h 做的第一件事就是包含serializable.h。这给出了Serializable 类的定义。然后尝试包含logger.h,但由于包含保护,这将失败。所以我们开始着手实现。哪个尝试调用Logger::PostLogMessageSimple 但尚未声明?我觉得还是有问题的。除非您想阻止除serializable.h 之外的任何人包括logger.h
  • @JaMiT,将 Serializable 的包含放在记录器中的包含保护之前会解决我认为
  • 当我尝试这种方法时,我在 Logger::PostLogMessageSimple: 'Logger' has not been declared 行出现编译器错误。似乎与此有关:stackoverflow.com/a/16628994/4875896
  • 将 Serializable 的包含放在 LogMessage 中的包含保护之前会导致许多其他循环依赖问题。
  • @AlanBirtles 听起来有道理,但是对记录器标题的更改也应该在这个答案中。
【解决方案3】:

考虑 N 个标头,每个标头都定义一个类。 让 N 个标头中的每一个都包含一个模板或内联函数,因此需要在标头中定义它,它使用所有其他 N-1 个类的声明。

在下面的示例中,我使用 N=4,结构而不是类和内联成员函数而不是模板,以实际使用其他结构。当然,每个结构定义也使用(需要)前向声明,但我省略了它,因为它与模式无关。

啊哈:

#ifndef A_H
#define A_H

// Forward declare everything.
struct A;
struct B;
struct C;
struct D;

struct A {
  void use();
};

#endif // A_H

#ifndef B_H
#include "B.h"
#endif
#ifndef C_H
#include "C.h"
#endif
#ifndef D_H
#include "D.h"
#endif

#ifndef A_defs_H
#define A_defs_H

inline void A::use()
{
  // Use everything.
  B x; C y; D z;
}

#endif // A_defs_H

B.h:

#ifndef B_H
#define B_H

// Forward declare everything.
struct A;
struct B;
struct C;
struct D;

struct B {
  void use();
};

#endif // B_H

#ifndef A_H
#include "A.h"
#endif
#ifndef C_H
#include "C.h"
#endif
#ifndef D_H
#include "D.h"
#endif

#ifndef B_defs_H
#define B_defs_H

inline void B::use()
{
  // Use everything.
  A x; C y; D z;
}

#endif // B_defs_H

C.h:

#ifndef C_H
#define C_H

// Forward declare everything.
struct A;
struct B;
struct C;
struct D;

struct C {
  void use();
};

#endif // C_H

#ifndef A_H
#include "A.h"
#endif
#ifndef B_H
#include "B.h"
#endif
#ifndef D_H
#include "D.h"
#endif

#ifndef C_defs_H
#define C_defs_H

inline void C::use()
{
  // Use everything.
  A x; B y; D z;
}

#endif // C_defs_H

D.h:

#ifndef D_H
#define D_H

// Forward declare everything.
struct A;
struct B;
struct C;
struct D;

struct D {
  void use();
};

#endif // D_H

#ifndef A_H
#include "A.h"
#endif
#ifndef B_H
#include "B.h"
#endif
#ifndef C_H
#include "C.h"
#endif

#ifndef D_defs_H
#define D_defs_H

inline void D::use()
{
  // Use everything.
  A x; B y; C z;
}

#endif // D_defs_H

每个#include 周围的守卫对于避免无限包含深度是必要的。注意只有两个标题(N = 2),您也可以将包含放在前一个块中。例如:

啊哈:

#ifndef A_H
#define A_H

// Forward declare everything.
struct A;
struct B;

struct A {
  void use();
};

#include "B.h"
#endif // A_H

#ifndef A_defs_H
#define A_defs_H

inline void A::use()
{
  // Use everything.
  B x;
}

#endif // A_defs_H

B.h 同样适用于N = 2

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-08-02
    • 2020-10-07
    • 2016-08-21
    • 1970-01-01
    • 2021-12-07
    • 2016-10-25
    • 1970-01-01
    • 2015-11-01
    相关资源
    最近更新 更多