I think I'm doing it wrong: Best way to store a collection of different child class objects?

Hello,

I'm currently developing a project which makes use of Parent Classes and Child Classes. I've been developing the code for about a year now, and have learned quite a lot along the way.

I'm creating many different types of child classes that all inherit from the Parent Class. All of these classes do different things. The main reason for inheriting from the parent class is so that I may store all the different objects in a container of type "Parent Class". This makes referencing the objects in runtime later on quite simple.

This has been working very well for me so far, until this point. I'm starting to develop child classes that use templates. The problem arises when I try to override a member function that's job is to return something with the template type. This causes an error (as expected) because there is no covariance between the overridden child class return type and the virtual function return type.

For those of you who understand this problem, may you please suggest an alternative approach? Or some other way of thinking about this problem that may help me solve it?

For those of you who are confused, I'd be happy to provide more information. At this point I'm not providing any code examples, just because I believe my question is more conceptual, rather than syntactical.

FYI, I'm programming the Raspberry PI Pico with Arduino IDE 2.1.0

Sincerely,
Marcus Garfunkel

You should boil it down to a minimum program that recreates the issue and post it.

Below is an example of my current class structure, that reproduces there error. Let me know if this helps. Any thoughts are appreciated.
Sincerely,
Marcus G

#include <map>
#include <memory>
class Module
{
    public:
        enum class moduleType{
            TYPE_DATA_ARRAY
                    
        };

        int getAddress()                {return m_address; }
        int getID()                     {return m_ID; }
        moduleType getModuleType()      {return m_moduleType; }

        // Configures the class to run for the first time. Only run once, after instantiating the class object.
 
        virtual int configure()
        {
            return -1;
        }

        virtual void process()
        {

        }

        virtual int getBufferValue(int index)
        {
            return -1;
        }

        virtual void* getBufferPointer()
        {
            return nullptr;
        }

    protected:
        const int m_address;
        int m_ID;
        const moduleType m_moduleType;

        Module(int address, moduleType moduleType)
            :m_address{address}, m_moduleType{moduleType}

        {
            
            if(m_addressIdMap.find(address) != m_addressIdMap.end())
            {
                m_ID = m_addressIdMap.at(address);
                m_addressIdMap.at(address)++;
            }            
            else
            {
                m_ID = 0;
                m_addressIdMap[address] = 1;
            }
        }
        
    private:
        static int newUID;
        static std::map<int,int> m_addressIdMap; 

};
int Module::newUID = 0;
std::map<int, int> Module::m_addressIdMap = {};


template<typename T>
class DataArray : public Module
{
    private:
        T*          m_Buffer;
        int         m_length;

    public:

        DataArray(int address, int length)
            :Module{address, moduleType::TYPE_DATA_ARRAY},
             m_length{length}
        {
            m_Buffer      =   new T[length];
        }

        ~DataArray()
        {
            delete [] m_Buffer;
        }

        int getBufferValue(int index)   override    {return m_Buffer[index];}

        T* getBufferPointer() override
        {
            return m_Buffer;
        }

        void process() override
        {

        }

};

void setup() {
  // put your setup code here, to run once:

    std::shared_ptr<Module>             da                  = std::make_shared<DataArray<volatile uint8_t>>   (0, 100);
}

void loop() {
  // put your main code here, to run repeatedly:

}

Two problems:

1. the getBufferPointer() function in the Module class promises to return a void *:

    virtual void* getBufferPointer() {

So, you can't override that with a function that returns a T *:

    T* getBufferPointer() override {   

2. The parameter of your templated DataArray class is type 'T', not 'volatile T'. So, you can't instantiate a DataArray with a volatile parameter.

  std::shared_ptr<Module> da = std::make_shared<DataArray<volatile uint8_t>>  (0, 100);

The code below compiles:

#include <map>
#include <memory>
class Module {
  public:
    enum class moduleType {
      TYPE_DATA_ARRAY
    };

    int getAddress() {
      return m_address;
    }
    int getID() {
      return m_ID;
    }
    moduleType getModuleType() {
      return m_moduleType;
    }

    // Configures the class to run for the first time. Only run once, after instantiating the class object.

    virtual int configure() {
      return -1;
    }

    virtual void process() {

    }

    virtual int getBufferValue(int index) {
      return -1;
    }

    virtual void* getBufferPointer() {
      return nullptr;
    }

  protected:
    const int m_address;
    int m_ID;
    const moduleType m_moduleType;

    Module(int address, moduleType moduleType)
      : m_address{address}, m_moduleType{moduleType} {
      if (m_addressIdMap.find(address) != m_addressIdMap.end()) {
        m_ID = m_addressIdMap.at(address);
        m_addressIdMap.at(address)++;
      } else {
        m_ID = 0;
        m_addressIdMap[address] = 1;
      }
    }

  private:
    static int newUID;
    static std::map<int, int> m_addressIdMap;

};
int Module::newUID = 0;
std::map<int, int> Module::m_addressIdMap = {};

template<typename T>
class DataArray : public Module {
  private:
    T *m_Buffer;
    int m_length;

  public:
    DataArray(int address, int length)
      : Module{address, moduleType::TYPE_DATA_ARRAY},
        m_length{length} {
      m_Buffer =  new T[length];
    }

    ~DataArray() {
      delete [] m_Buffer;
    }

    int getBufferValue(int index) override {
      return m_Buffer[index];
    }

    //T* getBufferPointer() override {
    void *getBufferPointer() override {
      return m_Buffer;
    }

    void process() override {

    }

};

void setup() {
  //std::shared_ptr<Module> da = std::make_shared<DataArray<volatile uint8_t>>  (0, 100);
  std::shared_ptr<Module> da = std::make_shared<DataArray<uint8_t>>  (0, 100);
}

void loop() {
}

Dear gfvalvo,

Thanks so much for the response. This information is very helpful. Now I am stuck at the next step, which is actually using this object. There are two options I see, both result in similar errors. Is there a smarter way of doing this?

See code snippet below. There are two "options" in the setup() function that will reproduce the new errors.

Option 1 error: Compilation error: 'void*' is not a pointer-to-object type

Option 2 error: Compilation error: invalid conversion from 'void*' to 'uint8_t*' {aka 'unsigned char*'} [-fpermissive]

#include <map>
#include <memory>
class Module {
  public:
    enum class moduleType {
      TYPE_DATA_ARRAY
    };

    int getAddress() {
      return m_address;
    }
    int getID() {
      return m_ID;
    }
    moduleType getModuleType() {
      return m_moduleType;
    }

    // Configures the class to run for the first time. Only run once, after instantiating the class object.

    virtual int configure() {
      return -1;
    }

    virtual void process() {

    }

    virtual int getBufferValue(int index) {
      return -1;
    }

    virtual void* getBufferPointer() {
      return nullptr;
    }

  protected:
    const int m_address;
    int m_ID;
    const moduleType m_moduleType;

    Module(int address, moduleType moduleType)
      : m_address{address}, m_moduleType{moduleType} {
      if (m_addressIdMap.find(address) != m_addressIdMap.end()) {
        m_ID = m_addressIdMap.at(address);
        m_addressIdMap.at(address)++;
      } else {
        m_ID = 0;
        m_addressIdMap[address] = 1;
      }
    }

  private:
    static int newUID;
    static std::map<int, int> m_addressIdMap;

};
int Module::newUID = 0;
std::map<int, int> Module::m_addressIdMap = {};

template<typename T>
class DataArray : public Module {
  private:
    T *m_Buffer;
    int m_length;

  public:
    DataArray(int address, int length)
      : Module{address, moduleType::TYPE_DATA_ARRAY},
        m_length{length} {
      m_Buffer =  new T[length];
    }

    ~DataArray() {
      delete [] m_Buffer;
    }

    int getBufferValue(int index) override {
      return m_Buffer[index];
    }

    //T* getBufferPointer() override {
    void *getBufferPointer() override {
      return m_Buffer;
    }

    void process() override {

    }

};

void setup() {
  //std::shared_ptr<Module> da = std::make_shared<DataArray<volatile uint8_t>>  (0, 100);
  std::shared_ptr<Module> da = std::make_shared<DataArray<uint8_t>>  (0, 100);

  //option 1:
  auto dataptr = da.get()->getBufferPointer();
  uint8_t value = dataptr[0];

  //option 2:
  //uint8_t* dataptr = da.get()->getBufferPointer();

}

void loop() {
}

Sincerely,
Marcus

Some Arduino board packages specify tighter levels of error checking than others. Try casting it to the correct pointer type:

  uint8_t* dataptr = reinterpret_cast<uint8_t *>(da.get()->getBufferPointer());

Dear gfvalvo,

Thanks again, this is working well.

My only concern is if this is the best approach for this kind of task? I feel like it could be dangerous to be using casting in general, as well as void pointers and whatnot. So I'm just wondering if there are any other concepts I could be looking at to make this more robust.

Any thoughts would be appreciated.

Thanks,
Marcus

It's not really clear where you're heading with all this. But, you may want to consider the concept of a variant. See My Post #16 in this Thread.

Hey gfvalvo,

I understand not having a clear understanding of the point of all this would be frustrating.

Basically, I’m developing a feature set that will be designed ahead of time (now), but the actual use and deployment will be set up at run time across many distributed units all networked together. I’d like a container to store representations of these “features” on each unit so that the main loop can cycle through each one and process them one by one.

There must be a design pattern for something this already, but haven’t discovered one yet.

What I have now, and using the casting does seem to work well. I’m sure I’ll come across some more issues soon though :laughing:

Curious why you chose the reinterpret_cast rather than a static_cast?

Thanks,
Marcus

That's the variant concept in the code of the forum post I linked above.

Yes, static_cast would work because the cast is just going through void * and then back to the original pointer type.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.