使用c++20,我有以下想法来概括对电路中多个芯片的访问:
接口应允许读取/写入具有不同地址位和寄存器大小(8、16、...)的所有芯片
template<unsigned int AddressBits = 9, unsigned int ContentBits = 32>
requires (AddressBits < ContentBits)
class MyInterface {
protected:
std::bitset<AddressBits> mAddress;
std::bitset<ContentBits> mValue;
public:
virtual ~MyInterface() = 0;
virtual std::bitset<ContentBits> read() = 0;
virtual bool write(const std::bitset<ContentBits> &value) = 0;
};
现在,假设我需要从上面的接口驱动几个具有不同地址和内容大小的芯片,即一个是 <8,16>,一个是 <9, 32>。
我该怎么做?这还有道理吗?我写了下面的代码,但它不支持我最初的想法:
class DRV8323 : public MyInterface<8,16> {
explicit DRV8323(unsigned int address, unsigned int resetValue = 0) {
mAddress = address;
mValue = resetValue;
}
~DRV8323() override = default;
std::bitset<16> read() override {
// Somehow read from hardware and return
return mValue;
}
bool write(const std::bitset<16> &value) override {
// Somehow write to hardware and return true/false to indicate result
return true;
}
};
// or vector<unique_ptr<MyInterface>>
std::vector<MyInterface*> AllRegisters = {
{ /* How to add two different derived classes/chips? */ },
{ /* How to add two different derived classes/chips? */ },
// And so on
};
MyInterface<8,16>
中扣除?我将把这两者都视为“我们应该吗?”代码设计主题并作为“我们可以吗?”主题。
接口不应涉及数据成员。它的目的是指定“如何与派生类交互”,而不是这些派生类在幕后必须是什么样子。对于数据成员,它只是一个抽象类(接口是它们的子集)。 C++ 并不关心这种区别,但程序员关心这种区别,因为它对派生类在底层的操作方式施加了限制。 抽象类的模板本身并不是抽象类。这是尝试在这里使用接口的最大问题 - 没有模板参数,您不会告知如何与东西交互,并且参数是派生类的。
尚不清楚您是否只是想要一些通用代码来消除重复/简化,或者这是否意味着向其他程序员提供有关如何使用您的驱动程序的最低必要信息(无论如何,最终可能会成为这种情况)。在后一种情况下,整个想法就会崩溃,因为您还需要向它们提供
AddressBits
和
ContentBits
,否则它们根本无法调用模板接口的方法。您最好创建一个非模板接口,该接口适用于通用类型而不是特定的位集,以及位数的吸气剂。您的 MyInterface
模板将从该模板派生并使用更专门的方法实现接口。它很笨拙,因为它有很多抽象。如果您只想使用抽象类进行重复数据删除,则可以将派生自同一基础的驱动程序混合在一起 - 不是相同的基础模板,但也具有相同的模板参数。这是因为每个实例化都是其自己的类型,您根本无法将它们全部解释为一种类型。您想要调用的函数具有不同类型的参数和返回值。这不是特定于 std::vector<T*>
,出于同样的原因无法创建 C 样式数组。
std::variant
和
std::visit
来解决它。使用 std::variant
,您需要在 std::vector
中命名指向您想要的所有基类(已实例化,请注意这个答案)的指针,因此如果您有许多此类驱动程序,它会很快变得巨大。然后使用 std::visit
调用每个指针的虚拟方法。您仍然面临在调用这些方法的代码中使用不同位集类型的问题,但这是一个不同的主题。这是一个示例代码,其中 DRV1234
源自 MyInterface<>
,但在其他方面与 DRV8323
相同:#include <vector>
#include <bitset>
#include <variant>
int main() {
auto *drv8323 = new DRV8323(8323);
auto *drv1234 = new DRV1234(1234);
std::vector<std::variant<MyInterface<8,16>*, MyInterface<>*>> V{
reinterpret_cast<MyInterface<8,16>*>(drv8323),
reinterpret_cast<MyInterface<>*>(drv1234),
};
for(const auto &ptr : V) std::visit([](auto &arg){ arg->read(); }, ptr);
for(const auto &ptr : V) std::visit([](auto &arg){ delete arg; }, ptr);
}
这里的下一个问题是析构函数。如this answer
中所述,需要实现MyInterface<...>
的析构函数(实际上是析构函数,每个实例化一个)才能成功链接。您可以添加以下内容:
template<unsigned int AddressBits, unsigned int ContentBits>
requires (AddressBits < ContentBits)
MyInterface<AddressBits, ContentBits>::~MyInterface() {}
最后关于您需要重复写入
AddressBits
和
ContentBits
的值的方式:您应该为它们命名并使用这些名称。有多种方法,例如为每个驱动程序创建一个命名空间并在那里定义全局 constexpr static
变量,为每个驱动程序创建参数结构并执行相同的操作,您可以(但不应该)使用宏,更深层次的嵌套继承将允许您使用相同的价值观和行为...我个人很喜欢参数结构方法,因为您可以将驱动程序类放入一个模板,该模板采用参数结构,其名称描述用途并根据需要指定它:
struct Bits8323 {
constexpr static unsigned int ADDRESS_BITS = 8;
constexpr static unsigned int CONTENT_BITS = 16;
};
template<class Bits>
class DRV8323Template : public MyInterface<Bits::ADDRESS_BITS,Bits::CONTENT_BITS> {
public:
explicit DRV8323Template(unsigned int address, unsigned int resetValue = 0) {
this->mAddress = address;
this->mValue = resetValue;
}
~DRV8323Template() override = default;
std::bitset<Bits::CONTENT_BITS> read() override {
// Somehow read from hardware and return
return this->mValue;
}
bool write(const std::bitset<Bits::CONTENT_BITS> &value) override {
// Somehow write to hardware and return true/false to indicate result
return true;
}
};
using DRV8323 = DRV8323Template<Bits8323>;
那么您仍然会遇到一个问题,您很可能需要在运行时告知模板参数。您无法从基类获取派生类的 constexpr 值,但您可以使(非 constexpr)getters 成为抽象基类的一部分,正如我上面提到的。