首页 文章

C - 为性能和封装构造常量数据的方法是什么?

提问于
浏览
2

很长一段时间,我编写了一些程序,其中包含一些程序范围的常量,例如 constants.h

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

这种方法的问题在于,在分配固定内存块或迭代循环时,代码的较低级别通常需要此信息,因此这些单元必须#include这个常见的 constants.h ,或者需要传递相关的值在调用这些函数时,使用在运行时永远不会改变的值来破坏接口,并损害编译器的优化能力 .

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

另一种选择是在运行时通过构造函数或其他成员函数提供相关常量 . 但是,处理性能对我来说很重要 - 我有一堆类都在编译时指定的固定大小的数组(缓冲区)上运行 . 目前,此大小要么取自 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文件) . 并且似乎只允许使用整数非类型参数,因此我不能以这种方式传递float或double常量 . 这是不允许的:

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

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

编辑:

这里使用类型来保存常量值,因此可以使用模板参数将它们传递给层 . 常量参数在 System 结构中定义,并作为同名的 System 模板参数提供给较低层 foobar

在顶层:

#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中的'bottom':

// 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?
    }
  }
};

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

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 .

相关问题