在这种情况下,有没有办法用一个解决方案替换仅在类型上不同的两个相似功能?

时间:2019-03-18 05:53:43

标签: c++ pointers templates iterator software-design

有一个名为PlotCurve的类。它将图表描述为点及其操作的容器。 PlotCurve的数据是从类RVDataProvider获得的。重要的是RVDataProvider提供的点数可能很大(超过1kk),因此RVDataProvider返回指向Y数据的只读指针(X数据可以通过指针)以提高性能。

主要问题是RVDataProvider对于两种类型有两种不同的方法:

class RVDataProvider : public QObject, public IRVImmutableProvider
{
public:
    // ...
    ReadonlyPointer<float>  getSignalDataFloat(int signalIndex, quint64 start, quint64 count) override;
 ReadonlyPointer<double> getSignalDataDouble(int signalIndex, quint64 start, quint64 count) override;
    // ...
}

ReadonlyPointer<T>只是C样式指针的只读包装。

为了获得一条曲线的值范围(用于寻找最小-最大,在画布上绘画等),我也应该声明不同的函数。

class PlotCurve : public QObject
{
public:
    // ...` 
    virtual ReadonlyPointer<float> getFloatPointer(quint64 begin, quint64 length) const;
    virtual ReadonlyPointer<double> getDoublePointer(quint64 begin, quint64 length) const;  
    // ...
}

如果添加了新的可用数据类型,则会导致在客户端代码中使用switch语句及其更改。

switch (dataType())
{
    case RVSignalInfo::DataType::Float: {
        auto pointer = getFloatPointer(begin, length);
        Q_ASSERT(!(pointer).isNull()); \
        for (quint64 i = 0; i < (length); ++i) { \
            auto y = (pointer)[i]; \
            if (y < (minY)) { (minY) = y; continue; } \
            if (y > (maxY)) { (maxY) = y; } \
        }
    } break;

    case RVSignalInfo::DataType::Double: {
        auto pointer = getDoublePointer(begin, length);
        Q_ASSERT(!(pointer).isNull()); \
        for (quint64 i = 0; i < (length); ++i) { \
            auto y = (pointer)[i]; \
            if (y < (minY)) { (minY) = y; continue; } \
            if (y > (maxY)) { (maxY) = y; } \
        }
    } break;

    // ...
}

有没有一种方法可以摆脱对客户端代码的依赖?我想到了三件事:

1)创建将是ReadonlyPointer的包装的Iterator类型。不,由于迭代器的虚拟功能,性能降低了10倍以上。

2)创建一个遍历方法,该方法将对某个范围内的每个值执行某些功能。再次,不行-使用函数指针进行最优化的版本比客户端代码中的switch语句慢两倍。

3)制作类PlotCurve模板。这样,我不能像现在这样向一个容器添加不同的PlotCurves。

1 个答案:

答案 0 :(得分:1)

不幸的是,对于OP问题,我看不出有什么可以做的。

充其量,案件的相似部分可以移至

  1. 功能模板

防止代码重复。

为演示起见,我将OP的问题与以下示例代码类似:

enum DataType { Float, Double };

struct Data {
  std::vector<float> dataFloat;
  std::vector<double> dataDouble;
  DataType type;

  Data(const std::vector<float> &data): dataFloat(data), type(Float) { }
  Data(const std::vector<double> &data): dataDouble(data), type(Double) { }
};

使用功能模板,处理过程可能如下所示:

namespace {

// helper function template for process()
template <typename T>
std::pair<double, double> getMinMax(const std::vector<T> &values)
{
  assert(values.size());
  double min = values[0], max = values[0];
  for (const T &value : values) {
    if (min > value) min = value;
    else if (max < value) max = value;
  }
  return std::make_pair(min, max);
}

} // namespace

void process(const Data &data)
{
  std::pair<double, double> minMax;
  switch (data.type) {
    case Float: minMax = getMinMax(data.dataFloat); break;
    case Double: minMax = getMinMax(data.dataDouble); break;
  }
  std::cout << "range: " << minMax.first << ", " << minMax.second << '\n';
}

Live Demo on coliru

有了宏,它将显得更加紧凑:

void process(const Data &data)
{
  std::pair<double, double> minMax;
  switch (data.type) {
#define CASE(TYPE) \
    case TYPE: { \
      assert(data.data##TYPE.size()); \
      minMax.first = minMax.second = data.data##TYPE[0]; \
      for (const double value : data.data##TYPE) { \
        if (minMax.first > value) minMax.first = value; \
        else if (minMax.second < value) minMax.second = value; \
      } \
    } break
    CASE(Float);
    CASE(Double);
#undef CASE
  }
  std::cout << "range: " << minMax.first << ", " << minMax.second << '\n';
}

Live Demo on coliru

许多人(包括我在内)都认为C ++中的宏很危险。与其他一切相反,宏不是名称空间或范围的主题。如果任何标识符意外地成为预处理的对象,则可能导致混淆。在最坏的情况下,意外修改的代码会通过编译器,并在运行时导致意外行为。 (我的悲伤经历。)

但是,在这种情况下,这是不期望的(假设代码将是源文件的一部分)。

我更喜欢第三种选择,它将重复的代码放在process()内。我想到了一个lambda,但lambda不能(但)尚未模板化:SO: Can lambda functions be templated?

不能选择本地模板(功能部件)。也禁止使用:SO: Why can't templates be declared in a function?


在OP反馈之后,有一个关于X macros的注释:这是C语言中防止数据冗余的一项古老技术。

定义了一个“数据表”,其中每一行是一个包含所有功能的(此处未定义)宏X的“调用”。

要使用数据表:

  1. 定义一个宏X,该宏仅使用个别情况下所需的参数(而忽略其余参数)
  2. #include数据表
  3. #undef X

再次提供示例:

void process(const Data &data)
{
  std::pair<double, double> minMax;
  switch (data.type) {
#define X(TYPE_ID, TYPE) \
    case TYPE_ID: { \
      assert(data.data##TYPE_ID.size()); \
      minMax.first = minMax.second = data.data##TYPE_ID[0]; \
      for (const double value : data.data##TYPE_ID) { \
        if (minMax.first > value) minMax.first = value; \
        else if (minMax.second < value) minMax.second = value; \
      } \
    } break;
#include "Data.inc"
#undef X
  }
  std::cout << "range: " << minMax.first << ", " << minMax.second << '\n';
}

其中Data.inc是:

X(Float, float)
X(Double, double)
X(Int, int)

Live Demon on coliru

尽管如此,这个宏-的小技巧让人有些恐惧-在维护方面,这非常方便。如果必须添加新的数据类型,那么X()中的新Data.inc行(当然还有重新编译)就足够了。 (希望编译器/构建链能够考虑来自Data.inc的源的所有依赖关系。我们在Visual Studio中从未遇到过与此相关的问题。)