在开发过程中使用#ifdef在不同类型的行为之间切换


28

在开发过程中使用#ifdef在不同类型的行为之间进行切换是一种好习惯吗?例如,我想更改现有代码的行为,我有几个想法如何更改行为,并且有必要在不同的实现之间切换以测试和比较不同的方法。通常,代码更改很复杂,并且会影响不同文件中的不同方法。

我通常会介绍几个标识符并执行类似的操作

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

void bar()
{
    doSomething3();
#ifndef APPROACH3
    doSomething4();
#endif
    doSomething5();
#ifdef APPROACH2
    bar_approach2();
#endif
}

int main()
{
    foo();
    bar();
    return 0;
}

这样就可以在不同的方法之间快速切换,并且仅用一份源代码即可完成所有操作。是发展的好方法还是更好的做法?



2
由于您在谈论开发,因此我相信您必须做一些您发现容易做的事情,才能切换和试验不同的实现。这更像是开发过程中的个人喜好,而不是解决特定问题的一些最佳实践。
艾默生·卡多佐

1
我建议使用策略模式或良好的多态性,因为这有助于为可切换行为保持单个插入点。
PMF

4
请记住,#ifdef如果关闭了块,某些IDE不会评估块中的任何内容。我们遇到了这样的情况:如果您不按常规方式构建所有路径,则代码很容易过时并且无法编译。
Berin Loritsch

看看我给另一个问题的答案。它提出了一些#ifdefs减少麻烦的方法。
user1118321

Answers:


9

对于该用例,我更喜欢使用版本控制分支。这样一来,您就可以在实现之间进行区分,为每个实现维护单独的历史记录,并且当您做出决定并需要删除其中一个版本时,只需丢弃该分支即可,而无需进行容易出错的编辑。


git尤其擅长这种事情。svnhg或其他情况可能并非如此,但仍然可以做到。
twalberg '17

那也是我最初的想法。“想摆弄些不同的东西吗?” git branch
Wes Toleman

42

当您拿着锤子时,一切看起来都像钉子。一旦您知道如何#ifdef将其用作在程序中获得自定义行为的一种方式,它就会很诱人。我知道,因为我犯了同样的错误。

我继承了一个用MFC C ++编写的旧程序,该程序已经用于#ifdef定义平台特定的值。这意味着只需定义(或在某些情况下未定义)特定的宏值,就可以将程序编译为在32位平台或64位平台上使用。

然后出现了问题,我需要为客户端编写自定义行为。我本可以创建一个分支并为客户端创建一个单独的代码库,但是那样会使维护工作陷入困境。我还可以定义启动时程序读取的配置值,并使用这些值来确定行为,但是随后我将不得不创建自定义设置,以将正确的配置值添加到每个客户端的配置文件中。

我很受诱惑,于是就屈服了。我#ifdef在代码中写了几节来区分各种行为。没错,一开始并没有什么过头的。进行了非常小的行为更改,使我可以将程序的版本重新分发给我们的客户,并且我不需要使用多个版本的代码库。

随着时间的流逝,无论如何,这变成了维护的麻烦因为程序不再在各个方面表现一致。如果我想测试程序的版本,则必须知道客户端是谁。尽管我尝试将代码缩减为一个或两个头文件,但代码却非常混乱,而且#ifdef提供的快速修复方法意味着此类解决方案像恶性肿瘤一样散布在整个程序中。

从那以后,我就已经上了课,你也应该。如果绝对必要,请使用它,并严格将其用于平台更改。解决程序(以及客户端)之间行为差异的最佳方法是仅更改启动时加载的配置。该程序保持一致,并且易于阅读和调试。


关于调试版本,例如“如果调试,定义变量x ...”,这似乎对日志记录很有用,但是当启用调试和不启用调试时,它也可能完全改变程序的工作方式。
WHN

8
@snb我想到了。我仍然更喜欢能够更改配置文件并使其更详细地记录日志。否则,生产中的程序会出问题,并且如果不完全替换可执行文件,则无法调试它。即使在理想情况下,这也不是所希望的。;)
尼尔

哦,是的,不用重新编译调试会更理想,我没有考虑这一点!
WHN

9
有关您所描述内容的极端示例,请查看本文“可维护性问题”小标题下的第二段,其中介绍了为什么MS达到几年前不得不从头开始重写大部分C运行时的原因。 。 blogs.msdn.microsoft.com/vcblog/2014/06/10/...
丹·尼利

2
@snb大多数日志记录库都假定使用日志记录级别机制。如果要在调试过程中记录某些信息,请以较低的日志记录级别进行记录(通常为“调试”或“详细”)。然后,应用程序有一个配置参数,告诉它要记录的级别。因此,答案仍然是此问题的配置。这还具有能够在客户端环境中打开此低日志记录级别的巨大好处。
jpmc26

21

暂时,您所做的一切都没有问题(例如,在签入之前):这是测试技术的不同组合或忽略部分代码的好方法(尽管这本身就说明了问题)。

但是有一点警告:不要让#ifdef分支停留在浪费我的时间上,而浪费我的时间只是通过四种不同的方式实现,只是为了弄清楚我应该阅读哪个

阅读#ifdef会很费力,因为您实际上要记住跳过它!不要使它变得比绝对必须的更难。

尽量少使用#ifdefs。通常,您可以在开发环境中执行此操作以实现永久差异,例如Debug / Release版本或不同的体系结构。

我编写了依赖于所包含库版本的库功能,这些功能需要#ifdef拆分。因此,有时这可能是唯一的方法,也可能是最简单的方法,但是即使那样,您也应该对保留它们感到沮丧。


1

像这样使用#ifdefs使得代码很难阅读。

因此,不,不要那样使用#ifdefs。

可能会有很多争论为什么不使用ifdefs,对我来说这已经足够了。

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

可以做很多事情可以做:

void foo()
{
    doSomething1();
    doSomething2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
}

void foo()
{
    doSomething1();
    doSomething2();
    foo_approach2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
    foo_approach2();
}

全部取决于是否定义了哪种方法。乍一看,它的功能绝对不清楚。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.