c++templatesvectorc++20abstract-class

using typed template argument with default value as abstract class in c++


Using c++20, I have the following idea to generalize access to multiple chips in a circuit:

An interface should allow reading/writing to all chips with different address bits and register sizes (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;
};

Now, let's say I need to drive from the above interface for a couple of chips with different addresses and content sizes, i.e. one is <8,16> and one is <9, 32>.

How should I do this? does it even make sense? I wrote the code below but it does not support my initial idea:

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
  };
  1. For the read function, I need to provide the number explicitly...Is there a way to automatically deduct it from the MyInterface<8,16>?
  2. Let's say I add another chip with a 9-bit address and 32-bit content. How can I then have a vector on the global scope that contains registers from both derived classes:

Solution

  • I'm going to address this both as a "should we?" code design topic and as a "could we?" topic.

    An interface shouldn't involve data members. It's meant for specifying how to interface with derived classes, not what those derived classes must be like under the hood. With data members, it's just an abstract class (interfaces are their subset). C++ doesn't care about that distinction, but programmers do because it puts restrictions on how the derived classes should operate under the hood.

    A template of an abstract class isn't an abstract class itself. That's the biggest problem with trying to use interfaces here - without template arguments, you don't inform how to interface with stuff, and the arguments are derived class's.

    It's not clear if you just want some common code to deduplicate/simplify or if this is meant to be minimum necessary information given to other programmers about how to work with your drivers (this could eventually become the case anyway). In the latter case, the whole idea falls apart because you need to provide AddressBits and ContentBits to them as well, otherwise they can't call the template-interface's methods at all. You'd be better off making a non-template interface that works with a common type instead of specific bitsets, and a getter for number of bits. Your MyInterface template would be derived from that and implement the interface using more specialised methods. It's clumsy because it's a lot of abstraction.

    If you just want an abstract class to deduplicate, it's possible to mix together drivers that derive from the same base - not the same template of base, but with the same template arguments too. That's because every instantiation is its own type, you simply can't interpret them all as one type. The functions you'd want to call have differently typed arguments and return values. This isn't specific to std::vector<T*>, a C-style array can't be created for the same reason.

    You can get around it with std::variant and std::visit. With std::variant, you need to name pointers to all the base classes (instantiated, note this answer) that you want in the std::vector, so it gets huge quickly if you have many such drivers. Then you use std::visit to call every pointer's virtual methods. You're still left with the problem of working with different bitset types in the code that calls those methods, but that's a different topic. Here's a sample code with DRV1234 derived from MyInterface<> but otherwise the same as 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);
    }
    

    The next problem here is with the destructor. As described in this answer, the destructor of MyInterface<...> (actually destructors, one for every instantiation) needs to be implemented to link successfully. You can add the following:

    template<unsigned int AddressBits, unsigned int ContentBits>
        requires (AddressBits < ContentBits)
    MyInterface<AddressBits, ContentBits>::~MyInterface() {}
    

    Finally regarding the way you need to repeatedly write the values of AddressBits and ContentBits: you should give names to them and use those names instead. There are multiple approaches, e.g. making a namespace for every driver and defining global constexpr static variables there, making param structs for every driver and doing the same, you can (but shouldn't) use macros, deeper nested inheritance will allow you to specify groups of drivers with the same values and behaviour...

    I'm personally a fan of the param struct approach since you can make your driver class into a template that takes param structs with names that describe purpose and specify it as needed:

    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>;
    

    Then you still run into the problem that you'll most likely need to inform about the template arguments in runtime. You can't get constexpr values of derived class from base class, but you can make (non-constexpr) getters part of the abstract base as I mentioned above.