使用虚拟基类函数从模板化派生类中提取数据

Extract data from templated derived class using virtual base class function

本文关键字:派生 数据 提取 虚拟 基类 类函数      更新时间:2024-09-30

我有一个Device对象,它可以有一个或多个State对象。我不想限制State对象可以描述什么样的状态,所以我已经将State对象的value模板化了。由于我希望每个Device都保留这些State对象的集合,所以我从GenericState类派生了它们。

我希望能够与指向这个GenericState类的指针进行交互,以便从模板化的值中读取和写入,但是不支持模板化的虚拟函数。我的解决方案是在基类中声明一个纯虚拟的"visiter"函数,该函数接受带有void*参数的函数。模板化派生类实现虚拟函数,并使用值State调用传入的函数。使用这种方法,我让调用者来选择如何处理值(强制转换为类型、读取、写入等(。下面的代码段。

我的问题是:

  1. 这种方法除了看起来"粗糙"且不是最可读之外,还有什么问题?

  2. 还有其他更好的方法来实现我的最终目标吗?我已经研究了std::variantstd::any,但std::variant似乎没有那么通用,std::any似乎不合适。我也考虑过静态/动态铸造,但我不确定它们的开销。

我的Device类"拥有"一组状态:

class Device {
std::list<std::unique_ptr<GenericState>> states;
...
}

GenericStateState类本身:

class GenericState {
public:
std::string name;
virtual ~GenericState() = default;
virtual void visitValue(std::function<void (void*)> func) = 0;
protected:
GenericState(std::string name): name(name) {}
};
template<typename T>
class State: public GenericState {
protected:
T value;
public:
State(const std::string& name, const T& value): GenericState(name), value(value) {}
const std::string getName() {return name; }
const T getValue() { return value; }
// Calls the provided function with a reference to the value for read/write
void visitValue(std::function<void (void*)> func) override {
func(&value);
}
};

GenericState指针如何用于读/写Statevalue:的示例

State state = State(name, 5.0);
GenericState* genericState = &state;
// state.value = 5.0
double newVal = 0.2;
genericState->visitValue([&newVal](void* val){*(double *)val = newVal;});
// state.value = 0.2
double testVal = 0.0;
genericState->visitValue([&testVal](void* val){testVal = *(double *)val;});
// testVal = 0.2

谢谢。

我的观察结果是,人们过度使用virtual,可能是因为C++通常是如何教授的。如果您必须在电信系统中提供30年的前向兼容性和模块负载而不重新启动,virtual非常有用;当具体类型已知时,尤其是在每次更改后重新编译整个项目时,它就不那么有用了。

这里需要注意的是,您将失去类型安全性给您的所有保证。在每次调用visitValue()时,你都必须指定值的类型,因为void*会隐藏它。类型安全的理念是,你应该进行类型检查(在类型规范的形式中称为"单元测试"(,如果你错过了,这些检查就会失败。现在,关键的观察是,你可能会以比States的数量更频繁的方式编写genericState->visitValue();。因此,您可能只列出一次状态,例如在variant中。

一旦你在variant中有了它,你就可以简单地visit它并有具体的类型——从那时起,你就有了类型安全性。所有这一切,你都赢得了列出所有状态一次

using GenericStateVar = std::variant<State, State2, State3>;
GenericStateVar genericState(State(name, 5.0));
double newVal = 0.2;
std::visit([&](auto&& state) { // you might limit the capture here
state.setValue(newVal); // consider making value public
}, genericState);
double testVal = 0.0;
std::visit([&](auto&& state) { // you might limit the capture here
testVal = state.getValue(); // consider making value public
}, genericState);

variant非常简单,不需要其他任何东西。此外,variant往往比virtual快一些,因为前者知道所有可能的类型(从技术上讲,它是switch相对于函数指针(。