2

很长一段时间以来,我编写的代码中定义了一些程序范围的常量,例如constants.h

const size_t kNumUnits = 4;
const float kAlpha = 0.555;
const double kBeta = 1.2345;

这种方法的问题在于,在分配固定内存块或遍历循环时,这些信息通常在较低级别的代码中需要,因此这些单元必须 #include this common constants.h,或者需要传入相关值当这些函数被调用时,接口的值在运行时从不改变,并损害了编译器的优化能力。

此外,让所有这些较低级别的代码依赖于一些常量的顶级定义标头对我来说似乎是一种难闻的气味。太像全局变量了,即使它们都是不变的。这使得编写可重用代码变得困难,因为需要引用一个或多个常量的每个组件都必须包含公共标头。将一堆组件汇集在一起​​时,必须手动创建和维护此标头。两个或多个组件可以使用相同的常量,但它们都不能自己定义它,因为它们都必须在每个程序中使用相同的值(即使程序之间的值不同),因此都需要#include 这个高级头文件以及它碰巧提供的所有其他常量 - 不适合封装。这也意味着组件不能“独立”使用,因为它们需要标头定义才能工作,但如果它们将它们包含在可重用文件中,则需要在将组件引入主项目时手动删除它们。这导致程序特定的组件头文件混乱,每次在新程序中使用组件时都需要手动修改这些头文件,而不是简单地从客户端代码中获取指令。

另一种选择是通过构造函数或其他成员函数在运行时提供相关常量。但是,处理性能对我来说很重要——我有一堆类,它们都在编译时指定的固定大小的数组(缓冲区)上运行。目前,这个大小要么取自 in 中的常量constants.h,要么在运行时作为函数参数传递给对象。我一直在做一些实验,将数组大小指定为非类型模板参数const变量,看起来编译器可以生成更快的代码,因为循环大小在编译时是固定的,并且可以更好地优化。这两个功能很快:

const size_t N = 128;  // known at compile time
void foo(float * buffer) {
  for (size_t i = 0; i < N; ++i) {
    buffer *= 0.5f;
  }
}

template <size_t N>  // specified at compile time
void foo(float * buffer) {
  for (size_t i = 0; i < N; ++i) {
    buffer *= 0.5f;
  }
}

与纯粹的运行时版本相反,因为 N 在编译时未知,所以不能很好地优化:

void foo(float * buffer, size_t N) {
  for (size_t i = 0; i < N; ++i) {
    buffer *= 0.5f;
  }
}

在编译时使用非类型模板参数传递此信息与#include 全局常量文件及其所有定义具有相同的性能结果const,但它更好地封装并允许暴露特定信息(仅此而已)到需要它的组件。

所以我想在声明类型时传入 N 的值,但这意味着我的所有代码都变成了模板化代码(以及将代码移动到 .hpp 文件所需要的)。而且似乎只允许整数非类型参数,所以我不能以这种方式传递浮点数或双精度常数。这是不允许的:

template <size_t N, float ALPHA>
void foo(float * buffer) {
  for (size_t i = 0; i < N; ++i) {
    buffer[i] *= ALPHA;
  }
}

所以我的问题是处理这个问题的最佳方法是什么?人们如何倾向于组织他们的程序范围的常量以减少耦合,同时仍然获得编译时指定常量的好处?

编辑:

这是使用类型来保存常量值的东西,因此能够通过使用模板参数将它们向下传递。常量参数在System结构中定义,并提供给较低层foobar作为同名System模板参数:

在顶层:

#include "foo.h"

struct System {
  typedef size_t buffer_size_t;
  typedef double alpha_t;

  static const buffer_size_t buffer_size = 64;
  //  static const alpha_t alpha = 3.1415;  -- not valid C++?
  static alpha_t alpha() { return 3.1415; } -- use a static function instead, hopefully inlined...
};

int main() {
  float data[System::buffer_size] = { /* some data */ };
  foo<System> f;
  f.process(data);
}

在 foo.h 中,在中间层:

// no need to #include anything from above
#include "bar.h"
template <typename System>
class foo {
public:
  foo() : alpha_(System::alpha()), bar_() {}

  typename System::alpha_t process(float * data) {
    bar_.process(data);
    return alpha_ * 2.0;
  }
private:
  const typename System::alpha_t alpha_;
  bar<System> bar_;
};

然后在“底部”的 bar.h 中:

// no need to #include anything 'above'
template <typename System>
class bar {
public:
  static const typename System::buffer_size_t buffer_size = System::buffer_size;
  bar() {}
  void process(float * data) {
    for (typename System::buffer_size_t i = 0; i < System::buffer_size; ++i) {
      data[i] *= static_cast<float>(System::alpha());  -- hopefully inlined?
    }
  }
};  

这确实有一个明显的缺点,就是将我未来的很多(全部?)代码转换为带有“系统”参数的模板,以防它们需要引用一个常量。它也很冗长且难以阅读。但它确实消除了对头文件的依赖,因为 foo 和 bar 不需要预先了解有关 System 结构的任何信息。

4

1 回答 1

0

您的大量描述激发了您为什么需要这些常量全局并首先在所有地方使用。如果许多模块真的需要这些信息,我想我只是不认为它是一种糟糕的代码气味。这显然是重要的信息,所以这样对待它。把它藏起来,把它散开或者(太糟糕了!)把它放在不止一个地方,似乎是为了它而改变。添加模板等似乎是额外的复杂性和开销。

需要考虑的几点:

  • 确保您使用的是预编译的头文件,并且它common.h在那里
  • 尝试common.h仅包含在实现文件中,并且仅包含在真正需要的地方
  • 确保仔细管理这些符号的可见性;不要在每个范围内公开常量,以防万一它们可能需要

另一个想法(受@Keith 上面的评论启发)是你可能想要考虑你的分层。例如,如果您有多个组件,每个组件在理想情况下都应该是自包含的,那么您可以拥有这些常量的本地版本,它们恰好是从这些全局常量初始化的。这样可以大大减少耦合并提高局部性/可见性。

例如,主组件可以有自己的接口:

// ProcessWidgets.h
class ProcessWidgets
{
    public:
        static const float kAlphaFactor;
    // ...
};

并在其实施中本地化:

// ProcessWidgets.cpp

#include "Common.h"

static const float ProcessWidgets::kAlphaFactor = ::kAlpha;

然后该组件中的所有代码仅引用ProcessWidgets::kAlphaFactor.

于 2013-04-11T03:13:59.037 回答