我必须使用很多#ifdef i386 和 x86_64 来获取特定于体系结构的代码,有时候需要使用#ifdef MAC或#ifdef WIN32 ...以便特定于平台代码。
我们必须保持公共代码库和便携式。
但是我们必须遵循使用#ifdef严格禁止的指导原则。我不明白为什么?
作为此问题的扩展,我还想了解何时使用#ifdef?
例如,dlopen()在从64位进程运行时无法打开32位二进制,反之亦然。因此它更具体的架构。在这种情况下我们可以使用#ifdef吗?
答案 0 :(得分:13)
使用#ifdef
而不是编写可移植代码,您仍然在编写多个特定于平台的代码。不幸的是,在许多(大多数?)情况下,您很快就会得到几乎无法穿透的便携式和平台特定代码。
您还经常将#ifdef
用于可移植性以外的目的(定义要生成的代码的“版本”,例如将包括什么级别的自我诊断)。不幸的是,这两者经常相互作用,并且交织在一起。例如,有人将一些代码移植到MacOS决定它需要更好的错误报告,他补充说 - 但它使其特定于MacOS。后来,其他人决定更好的错误报告在Windows上非常有用,所以如果定义了WIN32,他会自动#define
MACOS启用该代码 - 但随后添加“只需更多”{{1}在定义Win32时,排除一些真正 MacOS特定的代码。当然,我们还添加了MacOS基于BSD Unix的事实,因此当定义MACOS时,它也会自动定义BSD_44 - 但是(再次)转向并且排除某些BSD“的东西“在为MacOS编译时。
这很快退化为代码,如下面的示例(取自#ifdef Considered Harmful):
#ifdef WIN32
这是一个相当小的例子,只涉及少数宏,但阅读代码已经很痛苦了。我亲眼看到(并且必须处理)实际代码中的更多更糟糕。这里的代码很丑陋,阅读起来很痛苦,但要弄清楚在什么情况下使用哪些代码仍然相当容易。在许多情况下,你最终会得到更复杂的结构。
为了给出一个具体的例子,说明我更愿意看到这些内容,我会做这样的事情:
#ifdef SYSLOG
#ifdef BSD_42
openlog("nntpxfer", LOG_PID);
#else
openlog("nntpxfer", LOG_PID, SYSLOG);
#endif
#endif
#ifdef DBM
if (dbminit(HISTORY_FILE) < 0)
{
#ifdef SYSLOG
syslog(LOG_ERR,"couldn’t open history file: %m");
#else
perror("nntpxfer: couldn’t open history file");
#endif
exit(1);
}
#endif
#ifdef NDBM
if ((db = dbm_open(HISTORY_FILE, O_RDONLY, 0)) == NULL)
{
#ifdef SYSLOG
syslog(LOG_ERR,"couldn’t open history file: %m");
#else
perror("nntpxfer: couldn’t open history file");
#endif
exit(1);
}
#endif
if ((server = get_tcp_conn(argv[1],"nntp")) < 0)
{
#ifdef SYSLOG
syslog(LOG_ERR,"could not open socket: %m");
#else
perror("nntpxfer: could not open socket");
#endif
exit(1);
}
if ((rd_fp = fdopen(server,"r")) == (FILE *) 0){
#ifdef SYSLOG
syslog(LOG_ERR,"could not fdopen socket: %m");
#else
perror("nntpxfer: could not fdopen socket");
#endif
exit(1);
}
#ifdef SYSLOG
syslog(LOG_DEBUG,"connected to nntp server at %s", argv[1]);
#endif
#ifdef DEBUG
printf("connected to nntp server at %s\n", argv[1]);
#endif
/*
* ok, at this point we’re connected to the nntp daemon
* at the distant host.
*/
在这种情况下,可能我们对logerr的定义将是宏而不是实际函数。它可能是非常微不足道的,有一个标题,如:
if (!open_history(HISTORY_FILE)) {
logerr(LOG_ERR, "couldn't open history file");
exit(1);
}
if ((server = get_nntp_connection(server)) == NULL) {
logerr(LOG_ERR, "couldn't open socket");
exit(1);
}
logerr(LOG_DEBUG, "connected to server %s", argv[1]);
[目前,假设一个可以/将处理可变参数宏的预处理器]
鉴于你的主管的态度,即使可能也不可接受。如果是这样,那没关系。而是一个宏,而不是在函数中实现该功能。在其自己的源文件中隔离函数的每个实现,并构建适合目标的文件。如果您有很多特定于平台的代码,您通常希望将其隔离到自己的目录中,很可能使用自己的makefile 1 ,并且有一个顶级的makefile,它只需要选择哪个其他要根据指定目标调用的makefile。
答案 1 :(得分:7)
您应该尽可能避免使用#ifdef
。 IIRC,是Scott Meyers用#ifdef
写的,你没有获得与平台无关的代码。相反,您会获得依赖于多个平台的代码。 #define
和#ifdef
也不是语言本身的一部分。 #define
没有范围概念,这可能会导致各种各样的问题。最好的方法是将预处理器的使用保持在最低限度,例如包含保护。否则你很可能会陷入混乱,这很难理解,维护和调试。
理想情况下,如果您需要具有特定于平台的声明,则应该具有单独的特定于平台的包含目录,并在构建环境中正确处理它们。
如果您具有某些功能的特定于平台的实现,您还应将它们放入单独的.cpp文件中,并在构建配置中再次将它们哈希。
另一种可能性是使用模板。您可以使用空虚拟结构表示您的平台,并将其用作模板参数。然后,您可以将模板专门化用于特定于平台的代码。这样,您将依赖编译器从模板生成特定于平台的代码。
当然,任何此类工作的唯一方法是将特定于平台的代码非常干净地分解为单独的函数或类。
答案 2 :(得分:7)
我见过#ifdef
的3个广泛用法:
NDEBUG
任何人?)每个都有可能产生大量无法制作的代码,应该对其进行相应的处理,但并非所有代码都能以相同的方式处理。
<强> 1。平台特定代码
每个平台都有自己的一套特定的包含,结构和功能来处理IO(主要)等事情。
在这种情况下,处理这种混乱的最简单方法是提供统一的前端,并具有特定于平台的实现。
理想情况下:
project/
include/namespace/
generic.h
src/
unix/
generic.cpp
windows/
generic.cpp
这样,平台内容全部保存在一个文件中(每个标题),因此很容易找到。 generic.h
文件描述了接口,构建系统选择了generic.cpp
。否#ifdef
。
如果您需要内联函数(用于提高性能),那么提供内联定义和特定平台的特定genericImpl.i
可以包含在generic.h
文件的末尾,并且只有一个#ifdef
<强> 2。功能特定代码
这有点复杂,但通常只有图书馆才能体验。
例如,对于具有可变参数模板的编译器,Boost.MPL
更容易实现。
或者,支持移动构造函数的编译器允许您定义某些操作的更高效版本。
这里没有天堂。如果你发现自己处于这样的境地......你最终会得到类似Boost的文件(等等)。
第3。编译模式代码
一般情况下,您可以通过#ifdef
来逃避。传统的例子是assert
:
#ifdef NDEBUG
# define assert(X) (void)(0)
#else // NDEBUG
# define assert(X) do { if (!(X)) { assert_impl(__FILE__, __LINE__, #X); } while(0)
#endif // NDEBUG
然后,宏本身的使用不受编译模式的影响,所以至少混乱包含在一个文件中。
注意:这里有一个陷阱,如果在“ifdefed away”时宏没有扩展到计算语句的东西,那么你有可能在某些情况下改变流量。此外,当混合中存在函数调用(带副作用)时,宏不评估它们的参数可能会导致奇怪的行为,但在这种情况下这是可取的,因为所涉及的计算可能很昂贵。
答案 3 :(得分:2)
许多程序使用这样的方案来制作特定于平台的代码。更好的方法,也是一种清理代码的方法,是将特定于一个平台的所有代码放在一个文件中,命名函数相同并具有相同的参数。然后,您只需根据平台选择要构建的文件。
可能仍有一些地方无法将平台特定代码提取到单独的函数或文件中,您仍然可能需要#ifdef
部分,但希望它应该最小化。
答案 4 :(得分:1)
所以,假设有一个平台定义的类型:
我将使用高级别的typedef作为内部位并创建一个抽象 - 通常每#ifdef
/ #else
/ #endif
一行。
然后对于实现,在大多数情况下,我还将使用单个#ifdef
用于抽象(但这确实意味着平台特定的定义在每个平台上出现一次)。我还将它们分成单独的平台特定文件,这样我就可以通过将所有源代码放入项目并在没有打嗝的情况下构建来重建项目。在这种情况下,#ifdef
比尝试根据每个构建类型确定每个项目的每个项目的所有依赖项更容易。
因此,只需使用它来专注于您需要的平台特定抽象,并使用抽象,因此客户端代码是相同的 - 就像减少变量的范围一样;)
答案 5 :(得分:1)
不确定“#ifdef严格否定”是什么意思,但也许您指的是正在处理的项目的政策。
但您可能会考虑不检查Mac或WIN32或i386等内容。一般情况下,如果您使用的是Mac,则实际上并不在意。相反,您需要MacOS的某些功能,而您关心的是该功能的存在(或不存在)。出于这个原因,通常在构建设置中有一个脚本,它根据系统提供的功能检查功能和#defines事物,而不是根据平台对功能的存在进行假设。毕竟,您可能会认为MacOS上没有某些功能,但有人可能拥有一个MacOS版本,他们已经移植了该功能。检查此类功能的脚本通常称为“configure”,它通常由autoconf生成。
答案 6 :(得分:1)
其他人已表明首选解决方案:将依赖代码放入
一个单独的文件,包含在内。这个文件对应
不同的实现可以在不同的目录中(其中一个)
这是通过-I
或/I
指令指定的
调用),或通过动态构建文件的名称(使用
例如宏连接),并使用类似的东西:
#include XX_dependentInclude(config.hh)
(在这种情况下,XX_dependentInclude
可能被定义为:
#define XX_string2( s ) # s
#define XX_stringize( s ) XX_string2(s)
#define XX_paste2( a, b ) a ## b
#define XX_paste( a, b ) XX_paste2( a, b )
#define XX_dependentInclude(name) XX_stringize(XX_paste(XX_SYST_ID,name))
并在编译器中使用SYST_ID
或-D
初始化/D
调用。)
在上述所有情况中,将XX_
替换为您通常用于宏的前缀。
答案 7 :(得分:1)
我更喜欢拆分平台相关代码&amp;功能分为单独的翻译单元,让构建过程决定使用哪些单元。
由于拼写错误的标识符,我已经失去了一周的调试时间。编译器不会跨翻译单元检查已定义的常量。例如,一个单元可以使用“WIN386”和另一个“WIN_386”。平台宏是维护的噩梦。
此外,在阅读代码时,您必须检查构建说明和头文件以查看定义了哪些标识符。 标识符存在且具有值之间也存在差异。某些代码可能会测试标识符的存在,而另一个代码会测试同一标识符的值。未指定标识符时,后一个测试未定义。
只要相信他们是邪恶的,不愿意使用它们。