C++ 库中是否需要基于预处理器的功能开关?

Are preprocessor-based feature switches necessary in C++ libraries?

我一直在研究一些成熟的 C++ 项目,并且注意到一种模式,其中预处理器标志用于在编译时启用功能。

例如:

#ifdef MY_WIDGET
Widget createMyWidget() {
    // etc... 
}
#endif

然后在代码的其他地方:

#ifdef MY_WIDGET
widgets.push_back(createMyWidget());
// etc... 
#endif

对我来说,这似乎是不必要的,因为我们可以采用一种策略模式,使用继承或 std::function

当前用户的应用程序可能看起来像这样(库已使用 -DMY_WIDGET 编译):

#include <library/startApp.hpp>

int main() {
  startApp(); // createMyWidget will be called by the library
  return 0;
}

但是,我们可以重新设计库,以便用户可以这样写:

#include <library/startApp.hpp>
#include <my-widget-plugin/createMyWidget.hpp>

int main() {
  const std::vector<Widget> widgets = { createMyWidget() };
  startApp(widgets);
  return 0;
}

现在这个库没有任何编译开关,这使得构建更加简单,但是我们仍然可以扩展这个库的功能。这也可以防止在有人编译没有功能的库但随后错误地尝试使用该功能的情况下出现潜在的混淆。

适当使用const可以使编译后的二进制文件在任何一种情况下都一样高效。如果小部件应该延迟实例化,我们可以传递一个工厂向量。

基于预处理器的功能开关是否只是策略模式的一种控制较少的版本?

根据定义,策略模式是一种 运行 时间算法。另一方面,预处理器功能是编译时的。这意味着,它们在您编译时已解决,编译后,它们不再存在。我不确定你是否知道这一点,但我会说你知道。所以你的问题简化为:为什么我在编译时需要预处理器功能?

好吧,除了明显的多样性理由之外,考虑一种情况,您的策略模式涉及两个不同的库。现在,如果您使用预处理器命令,则不必 link 到您的程序不需要的分支。如果您使用策略模式,您必须 link 一切! 链接到您不需要的东西只是糟糕的风格。由于各种原因,它也可能不可行(正如 Sebastian 在评论中提到的,由于平台或许可或其他限制)。

主要区别在于做出选择的时间。

预编译器标志是你在编译时所做的选择,甚至在你启动你的程序之前,你可以有多个版本的程序,每个版本都做一些特定的事情,仅此而已,这在性能方面不能被任何其他选择击败稍后应用。

那么我们有:

The strategy pattern (also known as the policy pattern) is a behavioural software design pattern that enables an algorithm's behaviour to be selected at runtime.

Wikipedia

因此,在其最小的形式中,运行时有一个分支,最有可能以函数指针的形式出现。我同意这是一个最小的开销,但它是一个不必要的开销。 第二个最显着的区别是语义,因为一个发生在编译时,另一个发生在运行时传达的意图是不一样的,在第一种情况下,你基本上是在告诉人们你有可以为你的程序激活或不激活的特性,在第二种情况下,您是在告诉人们您有多种处理某事的方式,并且可以选择一种而不是其他方式。


总而言之,如果您将差异视为次要差异,则可以选择感觉更舒适的那个,因为每个选择都有其小缺陷(宏定义具有粗略的预处理器行为并添加了新的语言级别,策略模式有不必要的开销等),但如果两者都适合您,那么使用定义何时可以在编译时选择并使用策略可以在运行时选择。