编写平台特定代码的最佳(最干净)方法

8

假设你有一段代码必须根据程序运行的操作系统而不同。
这里有一个老派的做法:

#ifdef WIN32
   // code for Windows systems
#else
   // code for other systems
#endif

但是肯定有更为简洁的解决方案,对吧?

4
我讨厌条件编译,非常讨厌。我更喜欢使用公共头文件和单独的实现文件。这样更清晰、易于理解,并且通常可以减少代码重复。例如,oswrapper.h, oscommon.cpp, osposix.cpp, oswindows.cpp 可以结合使用,然后在 makefile 或者 IDE 的目标/配置文件中选择需要编译和链接的osposix 和 oswindows。 - user4581301
最干净的方法是找到一个已经实现你所需功能的便携式库。 - Baum mit Augen
1
@BaummitAugen 你手头上有没有这样的库来支持硬件、驱动程序等?那不是一个很有帮助的一般性陈述。 - Cloud
1
@Dogbert 如果那个评论是一个答案,我会将其发布为答案。但它不是,这只是一个简单的建议,使用现有的代码,而不是编写另一个在Windows上操纵__int64的头文件。 - Baum mit Augen
1
@BaummitAugen 好吧,说得对。 - Cloud
显示剩余4条评论
3个回答

4
我亲眼见过职业生涯中的半打公司采用的典型方法是使用硬件抽象层(HAL)。
这个想法是将最低级别的东西放入一个专用的头文件和静态链接库中,其中包括以下内容:
- 固定宽度整数(Linux上的int64_t,Windows上的__int64等)。 - 公共库函数(Linux vs Windows上的strtok_r() vs strtok_s())。 - 通用的数据类型设置(即:typedefs用于所有数据类型,例如xInt,xFloat等,用于整个代码,因此如果底层类型更改为平台或突然支持新平台,则无需重新编写和重新测试依赖于它的代码,这在劳动力方面可能非常昂贵)。
HAL本身通常充斥着像您的示例一样的预处理器指令,这就是事实。如果您用运行时if / else语句包装它,则由于未解决的符号,编译将失败。或者更糟糕的是,您可能会包含额外的符号,这将增加输出的大小,并且如果经常执行该代码,则可能会减慢程序的速度。
只要HAL编写得很好,HAL的头文件和库将为您提供一个公共接口和一组数据类型,以便在其余代码中使用最少的麻烦。
从职业角度来看,最美丽的方面是您的所有其他代码都不必考虑架构或操作系统的具体细节。您将在各种系统上拥有相同的代码流程,这将通过扩展允许您以各种不同的方式测试相同的代码,并发现您通常不会预期或测试的错误。从公司的角度来看,这可以节省大量劳动力成本,并且不会因为生产软件中的错误而失去客户。

你的回答很好,但在我看来,并不是所有平台缓解代码都可以称为“HAL”。例如,在处理与硬件无关的操作系统差异时,如Sleep(DWORD milliseconds)sleep(uint seconds)。如果我错了,请告诉我。 - Marc.2377
@Marc.2377 对于这样的情况,你只需要用自己的 sleep() 函数包装它们,使用一个公共的时间基准。例如:xsleep(xUint64_t 纳秒)。你的所有代码都使用这个函数,并且你特定平台的 HAL 将其转换 :) 唯一有问题的时候是当你尝试使用截然不同的系统(例如:具有不同线程、安全、网络模型的系统)时。在这种情况下,编写 HAL 就变得更加困难了。 - Cloud
确实,我曾经就是这样做的。然而,我的评论特别针对使用“HAL”术语来指代与硬件无关的抽象层。 - Marc.2377
@Marc.2377 有时候被称为“PAL”,但“HAL”是一个相当通用的术语,经常被使用,通常意味着操作系统和架构抽象化。 - Cloud

0

在我的职业生涯中,我不得不做很多这样的工作,支持在嵌入式设备上构建和运行的代码,以及在Windows上运行,并在不同的ASIC和/或ASIC修订版上运行。

我倾向于按照你的建议去做,当事情真正分歧时,我会继续定义我想要在平台之间固定的接口,然后有单独的实现文件甚至库。随着代码库变老并需要添加更多的异常情况,它可能会变得非常混乱。

有时您可以将此类内容隐藏在标头文件中,使您的代码看起来“干净”,但很多时候这只是在一堆宏魔法背后模糊了视线。

我唯一要补充的是,如果没有定义任何选项,则我倾向于使#ifdef / #else / #endif链失败。这迫使我在新版本出现时重新审查问题。有些人喜欢设置默认值,但我发现这只会隐藏潜在的失败。

当然,我正在嵌入式世界工作,其中代码空间至关重要(因为内存很小且固定),而代码清洁度不幸必须退居次要地位。


0

对于非平凡项目的一种被采用的做法是将特定于平台的代码写在单独的文件中(如果适用,还要写在单独的目录中),尽可能避免“本地化”的 #ifdef

比如你正在开发一个名为“Example”的库,example.hpp 将是你的库头文件:

example.hpp

#include "platform.hpp"

//
// here: platform-independent declarations, includes etc
//


// below: platform-specific includes    

#if defined(WINDOWS)

#include "windows\win32_specific_code.hpp"
// other win32 headers

#elif defined(POSIX)
#include "posix/linux_specific_code.hpp"
// other linux headers

#endif

platform.hpp(简体)

#if defined(WIN32) && !defined(UNIX)
#define WINDOWS
#elif defined(UNIX) && !defined(WIN32)
#define POSIX
#endif

win32_specific_code.hpp

void Function1();

win32_specific_code.cpp

#include "../platform.hpp"

#ifdef WINDOWS  // We should not violate the One Definition Rule
#include "win32_specific_code.hpp"
#include <iostream>

void Function1()
{
    std::cout << "You are on WINDOWS" << std::endl;
}

//...

#endif /* WINDOWS */

当然,在您的 linux_specific_code.hpp 文件中也要声明 Function1()

然后,在为 Linux 实现它时(在 linux_specific_code.cpp 文件中),一定要像我上面做的那样,将所有内容都包围在条件编译中(例如使用 #ifdef POSIX)。否则,编译器将生成多个定义,并且会出现链接错误。

现在,您的库的用户所需做的就是在他的代码中添加 #include <example.hpp>,并在编译器的预处理器定义中放置 #define WINDOWS#define POSIX。实际上,第二步可能根本不必要,假设他的环境已经定义了 WIN32UNIX 宏之一。这样,Function1() 就可以以跨平台的方式从代码中使用了。


这种方法基本上是Boost C++ Libraries所使用的方法。我个人认为它很干净、合理。然而,如果你不喜欢它,你可以阅读Chromium's conventions for multi-platform development,了解一种略有不同的策略。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接