如何处理模板 class header 中的循环 #include 调用?
How can I handle circular #include calls in a template class header?
与相关
我有一个可序列化的模板class:
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 class 使用从 Serializable(使用 CRTP)继承的 LogMessage object。
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
这里的问题是这些文件中的每一个都包含另一个导致构建错误的文件。不幸的是,我不能使用 above-linked 问题的解决方案(将 #include "Logger.h" 移动到 Serializable.cpp),因为 Serializable 是一个模板 class,因此需要定义在 header 文件中。
我不知道如何继续。感谢您的帮助!
编辑:
我还考虑过在 serializable.h 中使用 Logger 和 LogMessage 的前向声明,但是因为我在 Logger 中调用静态方法并使用 LogMessage::Severity,所以这不起作用。
有时循环依赖需要对涉及的组件进行一些分析。弄清楚圆圈为什么存在,然后弄清楚为什么它不需要存在。分析可以在多个级别进行。这是我要开始的两个级别。
(由于代码显然是从真实代码中简化而来的,我将避免假设它显示了真正问题的严重程度。说到这里,感谢您没有让我们淹没在大量细节中!代码足以说明问题的大体。我回答中的任何具体建议同样是为了说明问题,不一定是最终的解决方案。)
在一个层面上,您可以查看 classes 的意图。忘记代码并专注于目的。 class A 在不知道 class B 是什么的情况下无法定义自己是否有意义?请记住,这比知道 class B 存在(这等同于前向定义,不需要 header)更强大。如果不看代码就没有意义,那么也许您找到了可以处理的东西。不可否认,模板的使用使事情复杂化,因为整个实现需要在 header.
中
例如,Serializable
确实应该能够在不知道序列化将要做什么的情况下定义自己(即 Logger
)。但是,它是一个模板,其实现需要能够记录错误。好...棘手。
不过,这是一个可以寻找解决方案的地方。一种可能是将错误日志记录分成仅处理字符串(已经序列化的数据)的基础部分,以及可以将 LogMessage
转换为基础部分的字符串的翻译层。反序列化期间的错误强烈表明缺少任何要序列化的内容,因此日志记录可以直接转到基础部分。依赖圈会断链:
Serializable -> LoggerBase
Logger -> LoggerBase
Logger -> LogMessage -> Serializable -> LoggerBase
在另一个层面上,您可以详细查看代码,而不必太担心目的。您有 header A,包括 header B – 为什么? A 的哪些部分实际上使用了 B 的某些东西?实际使用了 B 的哪些部分?如果您需要更好地可视化这种依赖性所在,请绘制图表。然后考虑一些目的。这些部分是否在适当的位置定义?还有其他可能吗?
例如,在示例代码中,Serializable
需要 LogMessage
的原因是为了访问枚举 LogMessage::ERROR
。这不是需要整个 LogMessage
定义的充分理由。也许像 PostLogErrorSimple
这样的包装器可以消除知道严重性常量的需要?也许现实比这更复杂,但关键是一些依赖可以通过将依赖推入源文件来side-stepped。有时源文件用于不同的 class.
另一个例子来自Logger
class。此 class 需要 LogMessage
才能访问 LogMessage::Severity
枚举(即具有 ERROR
作为其值之一的枚举)。这也不是需要完整 class 定义的充分理由。也许枚举应该在别处定义?也许作为 Logger
的一部分?或者可能根本不在 class 定义中?如果这种方法奏效,依赖环就会断链:
Serializable -> Severity
Serializable -> Logger -> Severity // To get the PostLogMessageSimple function
Logger -> Severity
理想情况下,一旦处理完枚举,Logger
header 就可以通过 LogMessage
的前向声明而不是包括完整的 [=60] =]. (前向声明足以通过引用接收 object。并且大概完整的 Logger
定义将包含一些采用 LogMessage
参数的函数。)
如果你只是在你的 class 中有声明并且定义了不合时宜的方法你应该能够让它工作:
#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
这样做的额外好处是让您的 class 界面更清晰。
考虑 N header,每个定义一个 class。
让每个 N header 包含一个模板或内联函数,因此需要在 header 中定义,它使用所有其他 N-1 classes 的声明。
在下面的示例中,我使用 N=4 结构而不是 class 和内联成员函数而不是模板,以实际使用其他结构。当然,每个结构定义也使用(需要)前向声明,但我将其排除在外,因为它与模式无关。
A.h:
#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
周围的守卫对于避免无限包含深度是必要的。请注意,只有两个 headers (N = 2),您还可以将 include 放在前一个块中。例如:
A.h:
#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
。
与
我有一个可序列化的模板class:
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 class 使用从 Serializable(使用 CRTP)继承的 LogMessage object。
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
这里的问题是这些文件中的每一个都包含另一个导致构建错误的文件。不幸的是,我不能使用 above-linked 问题的解决方案(将 #include "Logger.h" 移动到 Serializable.cpp),因为 Serializable 是一个模板 class,因此需要定义在 header 文件中。
我不知道如何继续。感谢您的帮助!
编辑: 我还考虑过在 serializable.h 中使用 Logger 和 LogMessage 的前向声明,但是因为我在 Logger 中调用静态方法并使用 LogMessage::Severity,所以这不起作用。
有时循环依赖需要对涉及的组件进行一些分析。弄清楚圆圈为什么存在,然后弄清楚为什么它不需要存在。分析可以在多个级别进行。这是我要开始的两个级别。
(由于代码显然是从真实代码中简化而来的,我将避免假设它显示了真正问题的严重程度。说到这里,感谢您没有让我们淹没在大量细节中!代码足以说明问题的大体。我回答中的任何具体建议同样是为了说明问题,不一定是最终的解决方案。)
在一个层面上,您可以查看 classes 的意图。忘记代码并专注于目的。 class A 在不知道 class B 是什么的情况下无法定义自己是否有意义?请记住,这比知道 class B 存在(这等同于前向定义,不需要 header)更强大。如果不看代码就没有意义,那么也许您找到了可以处理的东西。不可否认,模板的使用使事情复杂化,因为整个实现需要在 header.
中例如,Serializable
确实应该能够在不知道序列化将要做什么的情况下定义自己(即 Logger
)。但是,它是一个模板,其实现需要能够记录错误。好...棘手。
不过,这是一个可以寻找解决方案的地方。一种可能是将错误日志记录分成仅处理字符串(已经序列化的数据)的基础部分,以及可以将 LogMessage
转换为基础部分的字符串的翻译层。反序列化期间的错误强烈表明缺少任何要序列化的内容,因此日志记录可以直接转到基础部分。依赖圈会断链:
Serializable -> LoggerBase
Logger -> LoggerBase
Logger -> LogMessage -> Serializable -> LoggerBase
在另一个层面上,您可以详细查看代码,而不必太担心目的。您有 header A,包括 header B – 为什么? A 的哪些部分实际上使用了 B 的某些东西?实际使用了 B 的哪些部分?如果您需要更好地可视化这种依赖性所在,请绘制图表。然后考虑一些目的。这些部分是否在适当的位置定义?还有其他可能吗?
例如,在示例代码中,Serializable
需要 LogMessage
的原因是为了访问枚举 LogMessage::ERROR
。这不是需要整个 LogMessage
定义的充分理由。也许像 PostLogErrorSimple
这样的包装器可以消除知道严重性常量的需要?也许现实比这更复杂,但关键是一些依赖可以通过将依赖推入源文件来side-stepped。有时源文件用于不同的 class.
另一个例子来自Logger
class。此 class 需要 LogMessage
才能访问 LogMessage::Severity
枚举(即具有 ERROR
作为其值之一的枚举)。这也不是需要完整 class 定义的充分理由。也许枚举应该在别处定义?也许作为 Logger
的一部分?或者可能根本不在 class 定义中?如果这种方法奏效,依赖环就会断链:
Serializable -> Severity
Serializable -> Logger -> Severity // To get the PostLogMessageSimple function
Logger -> Severity
理想情况下,一旦处理完枚举,Logger
header 就可以通过 LogMessage
的前向声明而不是包括完整的 [=60] =]. (前向声明足以通过引用接收 object。并且大概完整的 Logger
定义将包含一些采用 LogMessage
参数的函数。)
如果你只是在你的 class 中有声明并且定义了不合时宜的方法你应该能够让它工作:
#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
这样做的额外好处是让您的 class 界面更清晰。
考虑 N header,每个定义一个 class。 让每个 N header 包含一个模板或内联函数,因此需要在 header 中定义,它使用所有其他 N-1 classes 的声明。
在下面的示例中,我使用 N=4 结构而不是 class 和内联成员函数而不是模板,以实际使用其他结构。当然,每个结构定义也使用(需要)前向声明,但我将其排除在外,因为它与模式无关。
A.h:
#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
周围的守卫对于避免无限包含深度是必要的。请注意,只有两个 headers (N = 2),您还可以将 include 放在前一个块中。例如:
A.h:
#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
。