我最近发布了一个问题,询问哪些动作将构成C++ 的禅宗。我收到了很好的答案,但我无法理解一项建议:
- 使头文件自给自足
你如何确保你的头文件是自给自足的?
欢迎任何其他与 C/C++ 中头文件的设计和实现相关的建议或最佳实践。
编辑:我发现这个问题解决了我的“最佳实践”部分。
自给自足的头文件是一个不依赖于包含它的上下文才能正常工作的头文件。如果您确保在使用它之前#include 或定义/声明所有内容,那么您自己就有了一个自给自足的标题。非
自给自足标头
的示例可能是这样的:
----- MyClass.h -----
class MyClass
{
MyClass(std::string s);
};
-
---- MyClass.cpp -----
#include <string>
#include "MyClass.h"
MyClass::MyClass(std::string s)
{}
在此示例中,MyClass.h使用std::string
没有第一个 #include 。为此,您需要在MyClass.cpp#include <string>
中放置before #include "MyClass.h"
。
如果 MyClass 的用户未能执行此操作,他将收到std::string is not included错误。
保持你的标题是自给自足的往往会被忽略。例如,您有一个巨大的 MyClass 标头,并向其中添加另一个使用 std::string 的小方法。在当前使用这个类的所有地方,在 MyClass.h 之前已经#included。然后有一天你#include MyClass.h 作为第一个标题,突然你在一个你甚至没有接触过的文件中出现了所有这些新错误(MyClass.h)
仔细维护你的标题以自给自足地帮助避免这个问题。
NASA 的戈达德太空飞行中心(GSFC) 已发布了解决此问题的 C 和 C++ 编程标准。
假设您有一个带有源文件perverse.c
及其标头的模块perverse.h
。
有一种非常简单的方法可以确保标头是自包含的。在源文件中,您包含的第一个标头是模块的标头。如果它像这样编译,则标头是自包含的(自给自足)。如果没有,请修复标题,直到它(可靠地1)自包含。
#ifndef PERVERSE_H_INCLUDED
#define PERVERSE_H_INCLUDED
#include <stddef.h>
extern size_t perverse(const unsigned char *bytes, size_t nbytes);
#endif /* PERVERSE_H_INCLUDED */
几乎所有的标题都应该受到保护以防止多重包含。(标准<assert.h>
标头是该规则的明确例外——因此是“几乎”限定词。)
#include "perverse.h"
#include <stdio.h> // defines size_t too
size_t perverse(const unsigned char *bytes, size_t nbytes)
{
...etc...
}
请注意,尽管传统上认为在项目标头之前包含标准标头是一个好主意,但在这种情况下,模块标头 ( perverse.h
) 位于所有其他标头之前对于可测试性至关重要。我允许的唯一例外是在模块头之前包含一个配置头;然而,即使这样也是值得怀疑的。如果模块头需要使用(或者可能只是“可以使用”)配置头中的信息,它可能应该包含配置头本身,而不是依赖使用它的源文件来执行此操作。但是,如果您需要配置请求支持的 POSIX 版本,则必须在包含第一个系统标头之前完成。
脚注 1:Steve Jessop对Shoosh的回答的评论是为什么我将括号中的“(可靠)”评论放入我的“修复它”评论中。他说:
使这变得困难的另一个因素是 C++ 中的“系统头文件可以包含其他头文件”规则。如果
<iostream>
include<string>
,那么很难发现您忘记包含<string>
在 [not] 使用<iostream>
[or<string>
] 的某些标头中。自己编译头文件不会出错:在这个版本的编译器上它是自给自足的,但在另一个编译器上它可能不起作用。
另请参阅Toby Speight关于 IWYU的回答——包括您使用的内容。
预编译头文件的 GCC 规则允许每个翻译单元只有一个这样的头文件,并且它必须出现在任何 C 标记之前。
只有当这些条件适用时,才能使用预编译的头文件:
- 在特定编译中只能使用一个预编译头文件。
大致上,这些约束意味着预编译的头文件必须是文件中的第一个。第二个近似值指出,如果 'config.h' 仅包含 #define 语句,它可能出现在预编译头文件之前,但更有可能的是(a)来自 config.h 的定义会影响其余代码,并且(b) 预编译的头文件无论如何都需要包含 config.h。
我从事的项目没有设置为使用预编译的头文件,而且 GCC 定义的约束加上 20 多年的密集维护和各种编码人员的扩展导致的无政府状态意味着很难添加它们.
鉴于 GSFC 指南和 GCC 预编译头文件之间的不同要求(并假设使用了预编译头文件),我认为我会使用单独的机制来确保头文件的自包含和幂等性。我已经为我从事的主要项目这样做了——重新组织标题以符合 GSFC 指南并不是一个简单的选择——我使用的脚本是chkhdr
,如下所示。您甚至可以将其作为头目录中的“构建”步骤来执行——确保所有头文件都作为“编译”规则自包含。
我使用这个chkhdr
脚本来检查标题是否是独立的。虽然 shebang 说的是“Korn shell”,但代码实际上可以使用 Bash 甚至原始(System V-ish)Bourne Shell。
#!/bin/ksh
#
# @(#)$Id: chkhdr.sh,v 1.2 2010/04/24 16:52:59 jleffler Exp $
#
# Check whether a header can be compiled standalone
tmp=chkhdr-$$
trap 'rm -f $tmp.?; exit 1' 0 1 2 3 13 15
cat >$tmp.c <<EOF
#include HEADER /* Check self-containment */
#include HEADER /* Check idempotency */
int main(void){return 0;}
EOF
options=
for file in "$@"
do
case "$file" in
(-*) options="$options $file";;
(*) echo "$file:"
gcc $options -DHEADER="\"$file\"" -c $tmp.c
;;
esac
done
rm -f $tmp.?
trap 0
碰巧我从来不需要将任何包含空格的选项传递给脚本,因此代码在处理空格选项时并不合理。在 Bourne/Korn shell 中处理它们至少会使脚本更加复杂而没有任何好处;使用 Bash 和数组可能会更好。
用法:
chkhdr -Wstrict-prototypes -DULTRA_TURBO -I$PROJECT/include header1.h header2.h
上面链接的 URL 不再起作用 (404)。您可以在EverySpec.com (第 2 页)上找到 C++ 标准 (582-2003-004 );C 标准 (582-2000-005) 似乎在行动中缺失。
但是,可以通过 Internet 档案访问和下载引用的 NASA C 编码标准:
也可以看看:
确保在标题中包含您需要的所有内容,而不是假设您包含的内容包含您需要的其他内容。
老问题,新答案。:-)
现在有一个名为include-what-you-use的工具,旨在针对此类问题分析您的代码。在 Debian 和派生系统上,它可以作为iwyu
软件包安装。
这个想法是头文件不依赖于以前的头文件来编译。因此头文件的顺序并不重要。这样做的一部分是在头文件中包含它需要的所有其他头文件。另一部分是 ifdef'ing 你的标题,这样它们就不会被多次处理。
这个想法是,如果你需要在你的类中添加一个 foo 对象,你只需要 #include foo.h 并且你不需要在它前面加上 bar.h 来让 foo.h 编译(例如那里是 foo 中的一个调用,它返回一个 bar 对象实例。您可能对此调用不感兴趣,但您需要添加 bar.h 以让编译器知道正在引用什么)。
我不确定我是否总是同意这个建议。一个大型项目将有数百个头文件,编译将最终读取其中的常见文件数百次,只是为了忽略#ifdefs。我在这种情况下看到的是一个头文件的头文件,它是项目标准的,包括三十个常见的头文件。它始终位于包含列表的首位。这可以加快编译时间,但使通用头文件的维护成为一项熟练的任务。
您想使用GNU C Preprocessor Manual中描述的方法:
2.4 一次性标头
如果头文件恰好被包含两次,编译器将处理其内容两次。这很可能导致错误,例如当编译器两次看到相同的结构定义时。即使没有,也肯定会浪费时间。
防止这种情况的标准方法是将文件的全部真实内容包含在条件中,如下所示:
/* File foo. */ #ifndef FILE_FOO_SEEN #define FILE_FOO_SEEN
整个文件
#endif /* !FILE_FOO_SEEN */
这种构造通常称为包装器
#ifndef
。当再次包含头文件时,条件将为假,因为FILE_FOO_SEEN
已定义。预处理器会跳过文件的全部内容,编译器不会看到它两次。CPP 进一步优化。它会记住头文件何时具有包装器“<code>#ifndef”。如果后续的 '<code>#include' 指定了该标头,并且仍然定义了 '<code>#ifndef' 中的宏,则根本不需要重新扫描文件。
您可以将注释放在包装器之外。它们不会干扰这种优化。
该宏
FILE_FOO_SEEN
称为控制宏或保护宏。在用户头文件中,宏名称不应以“<code>_”开头。在系统头文件中,它应该以 '<code>__' 开头,以避免与用户程序冲突。在任何一种头文件中,宏名都应包含文件名和一些附加文本,以避免与其他头文件发生冲突。
这是一个很好的问题。我想我会重新检查stdafx.h
在使用 Visual Studio 时将 a 作为第一个包含在每个 .cpp 文件中的做法。如果你用的是预编译好的头文件,反正也没关系,还不如有更友好的头文件。
感谢贾尔夫的指正。来自维基百科
Visual C++ 不会编译源文件中#include "stdafx.h" 之前的任何内容,除非未选中编译选项 /Yu'stdafx.h'(默认情况下);它假定源代码中直到并包括该行的所有代码都已编译。
所以这意味着预编译的头文件打破了自给自足的头文件规则,对吧?
没有看到你的其他问题,我首先想到的是保护我的头文件免受多次调用(让我的头文件自己解决)。
#ifndef MY_PROTECTED_HEADER_H
#define MY_PROTECTED_HEADER_H
/*
* Stuff here
*/
#endif /* MY_PROTECTED_HEADER_H */