c++ - 在这种情况下,有没有办法用一个解决方案替换两个仅在类型上不同的相似功能?
问题描述
有一个类名为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) 创建 Iterator 类型,该类型将是ReadonlyPointer
. 不——由于迭代器的虚函数,性能降低了 10 倍以上。
2)创建一个遍历方法,该方法将对某个范围内的每个值执行某些功能。switch
再次没有 - 使用函数指针的最优化版本比客户端代码中的语句慢两倍。
3)制作类PlotCurve
模板。通过这种方式,我不能像现在这样将不同的 PlotCurves 添加到一个容器中。
解决方案
不幸的是,对于 OPs 问题,我看不出有什么可以做的。
充其量,可以将案件中外观相似的部分移至
- 一个宏
- 一个函数模板
以防止代码重复。
为了演示,我使用以下示例代码来模拟 OPs 问题:
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';
}
使用宏,它会显得更加紧凑:
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';
}
许多人(包括我在内)认为 C++ 中的宏很危险。与其他一切相反,宏不是名称空间或范围的主题。如果任何标识符意外成为预处理的对象,这可能会导致混淆。在最坏的情况下,意外修改的代码会通过编译器并导致运行时出现意外行为。(我的悲惨经历。)
但是,在这种情况下不会出现这种情况(假设代码是源文件的一部分)。
我更喜欢第三种选择,它将重复的代码放在process()
. 一个 lambda 出现在我的脑海中,但 lambda 不能(还)被模板化:SO: lambda 函数可以被模板化吗?.
本地模板(函子)不是替代品。它也被禁止:SO:为什么不能在函数中声明模板?.
OP反馈后,关于X宏的说明:这是C语言中防止数据冗余的古老技术。
X
定义了“数据表”,其中每一行是包含所有特征的(此处未定义)宏的“调用” 。
要使用数据表:
- 定义一个宏 X,它只使用个别情况下需要的参数(并忽略其余的)
#include
数据表#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)
虽然,这种宏观技巧有点吓人——这在维护方面非常方便。如果必须添加新的数据类型,则只需添加新X()
行Data.inc
(当然,还要重新编译)。(编译器/构建链有望考虑所有源代码的依赖关系Data.inc
。我们在 Visual Studio 中从未遇到过这个问题。)
推荐阅读
- laravel - 如何仅在laravel中的前一周记录上添加编辑按钮
- c# - .NET Core 中不支持此类接口 (0x80004002)
- android - 未处理的异常:键入“列表”
' 不是类型 'List 的子类型 '颤动中的共享偏好 - python - PyQt QPushButton leaveEvent 同时按下 QPushButton
- mysql - 使用多个表的 SQL 查询
- java - 将 NDI 流保存到 mp4 时出现 FFMPEG 错误
- c# - 使用 C# 将文档插入 CosmosDB
- javascript - 如何在反应中访问它之外的.map()方法的索引
- vb.net - 位运算符 AND 不返回值
- css - CSS border-radius 改变背景角