每当我创建 .h 头文件时,我都会想到一个问题:“如何通过分层包含停止传播声明?”假设下面有这些文件:
foo.h中
#ifndef FOO_H
#define FOO_H
typedef int foo_t;
inline int foo() { return 1; }
class foo_c {};
#endif /* FOO_H */
bar.h
#ifndef BAR_H
#define BAR_H
#include "Foo.h"
typedef foo_t bar_t;
inline int bar() { return foo(); }
class bar_c : public foo_c {};
#endif /* BAR_H */
zoo.h
#ifndef ZOO_H
#define ZOO_H
#include "Bar.h"
typedef bar_t zoo_t;
inline int zoo() { return bar(); }
class zoo_c : public bar_c {};
#endif /* ZOO_H */
在文件 zoo.h 中,我们可以访问声明的元素foo_c
,foo_t
,foo()
以及对 foo.h的每次更改< / em>将重新编译 zoo.h
我知道我们可以将实现移动到 .cpp 文件,但是 .h 文件中的类定义中编写的代码怎么样?如果需要,我们如何强制程序员在 zoo.h 中明确地包含 foo.h ?
作为Qt中的一个示例,当我添加并使用<QQueue>
时,我无权QList
QQueue
继承QList
并且我必须包含<QList>
明确{1}} (另外,我不知道它是如何完成的,以及它对编译时间的影响)
答案 0 :(得分:3)
在C ++和C中,“要停止传播声明”,您需要将它们从公共接口,句点中删除。将它们移动到实现。或者“不太公开”的界面。
编制时间是目标之一。其他是便携性,可维护性。这也与loose coupling直接相关。
最受欢迎的C ++技术可以帮助您进行类推导Pimpl idiom。派生您的实现类,在公共接口中包含相应的头文件到实现cpp和forward-declare实现。您的用户对基类一无所知,只会知道您的实现名称。
如果您想使用typedef
,则无法停止传播。但是为了提供更好的可移植性和可维护性,您可以使用与Boost库有效使用相同的方法:实现定义类型(例如this one)。
每个界面设计都是extensibility,information hiding和简单(或努力)之间的权衡。如果你需要先归档两个使用更复杂的方法。您可以提供两个公共接口:一个用于使用,另一个用于更广泛和更低级别的可扩展性。
答案 1 :(得分:3)
我发现在我的代码中明确区分前向声明与定义非常重要:尽可能多地使用前向声明。
一般来说,如果您的X类不需要知道Y类的大小,那么您只需要Y的前向声明 - 您不需要包含Y.hpp。
例如,如果X不是Y的子类,而X不包含Y类型的任何成员,那么您不需要包含Y.hpp。前向声明 Y类; 就足够了。有时候,为了更好地解耦我的代码,我会保留一个引用或指向Y而不是在类X中嵌入Y - 如果这是可行的,那么我需要做的就是前向声明类Y;
现在,有一条关于在使用模板类时无法转发声明的评论。但是有一个诀窍 - 而不是使用typedef,你想要的模板实例化的子类,例如:
class Bars : public std::vector<Bar> { };
现在您可以转发声明class Bars;
,之前您无法转发声明std::vector<Bar>;
所以,这些是我在所有C ++项目中遵循的步骤:
#include <modulename/fdecl.hpp>
而不是#include <modulename/foo.hpp>
(对定义的前向声明)通过这种方式,标头松散耦合,我修改代码时编译时间更快。
答案 2 :(得分:2)
我会用这种方式重写代码:
foo.h中
#ifndef FOO_H
#define FOO_H
inline int foo();
#endif /* FOO_H */
Foo.cpp中
#include "foo.h"
inline int foo()
{
return 1;
}
bar.h
#ifndef BAR_H
#define BAR_H
inline int bar();
#endif /* BAR_H */
bar.cpp
#include "bar.h"
#include "foo.h"
inline int bar()
{
return foo();
}
zoo.h
#ifndef ZOO_H
#define ZOO_H
inline int zoo();
#endif /* ZOO_H */
zoo.cpp
#include "zoo.h"
#include "bar.h"
inline int zoo()
{
// cannot *incidentally* access foo() here, explicit #include "foo.h" needed
return bar();
}
这样,您只在头文件中显示您的界面,并且实现细节保留在.cpp文件/中。
但请注意,如果您使用模板,此策略将失败:它们必须在标头中完全声明(否则您可能会遇到链接器问题)。
答案 3 :(得分:2)
也许你可以使用命名空间:
foo.h中
namespace f {
inline int foo();
}
bar.h
#include "foo.h"
inline int bar()
{
using namespace f;
return foo();
}
zoo.h
#include "bar.h"
inline int zoo()
{
using namespace b;
// Cannot use foo here: can only refer to it by the full name f::foo
return bar();
}
这个例子看起来很人为,但可能只是因为代码太短了。如果您的应用程序涉及更多代码,这个技巧可能会有所帮助。
<强>更新强>:
同样的原则可以用于类和其他名称。例如,使用Qt名称:
qt_main.h
namespace some_obscure_name
{
class QList {...};
class QQueue: public QList {...}
...
}
qt_list.h
#include "qt_main.h"
using some_obscure_name::QList;
qt_queue.h
#include "qt_main.h"
using some_obscure_name::QQueue;
zoo.h:
#include "qt_queue.h"
...
QQueue myQueue; // OK
QList myList1; // Error - cannot use QList
some_obscure_name::QList myList2; // No error, but discouraged by Qt developers
免责声明:我对Qt没有经验;这个例子没有显示Qt开发人员实际做了什么,它只显示他们可以做什么。
答案 4 :(得分:1)
你不能吃蛋糕也吃。您可以尽可能多地利用内联,或者尽可能地限制可见性。对于类,您必须在使用派生和/或直接数据成员(需要相应的类定义可用)或间接数据成员(即指针或引用)之间取得平衡,这些成员只需要声明类。你的方法倾向于内联/直接包含,相反的极端是:
foo.h中
#ifndef FOO_H
#define FOO_H
typedef int foo_t;
int foo();
class foo_c {};
#endif /* FOO_H */
bar.h
#ifndef BAR_H
#define BAR_H
typedef foo_t bar_t;
int bar();
class foo_c;
class bar_c {
public:
bar_c();
private:
foo_c * my_foo_c;
};
#endif /* BAR_H */
zoo.h
#ifndef ZOO_H
#define ZOO_H
typedef bar_t zoo_t;
int zoo();
class zoo_c {
public:
zoo_c();
private:
bar_c * my_bar_c;
};
#endif /* ZOO_H */
foo.c的
#include "foo.h"
int foo() {
return 1;
}
bar.c
#include "bar.h"
#include "foo.h"
int bar() {
return foo();
}
bar_c::bar_c() : my_foo_c(new foo_c()) {}
zoo.c
#include "zoo.h"
#include "bar.h"
int zoo()
{
return bar();
}
zoo_c::zoo_c() : my_bar_c(new bar_c()) {}
介于两者之间的方法是引入一个额外级别的源文件,您可以将其称为.inl
,在那里移动函数实现并使它们内联。通过这种方式,您可以在原始标题之后包含这些新文件,并且仅在实际需要的位置包含这些新文不过,我认为这不值得付出努力。
模板会使事情进一步复杂化,因为通常定义必须在模板需要实例化的任何地方都可用。有办法控制这种情况,例如:通过强制实现所需专业化的实例化,以避免包含每个使用点的定义,但同样增加的复杂性可能是不值得的。
如果您担心compilaton时间通常会更容易依赖编译器的标头预编译机制。