How do I remove this inheritance-related code smell?

Okay then let's simplify all the unnecessary complications :)

Your code really boils down to the following:

SomeInfo.h

struct SomeInfo
{
    const char *name;
    const f32_t value;
};

void processData(const SomeInfo* c_myInfo, u8_t len);

SomeInfo.cpp

#include "SomeInfo.h"

void processData(const SomeInfo* c_myInfo, u8_t len)
{
    for (u8_t i = 0; i < len; i++)
    {
        DPRINTF("Name: %s - Value: %f \n", c_myInfo[i].name, c_myInfo[i].value);
    }
}

data.h

#include "SomeInfo.h"

struct A
{
    const SomeInfo info[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
    static const u8_t len = 2;
};

struct B
{
    const SomeInfo info[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} };
    static const u8_t len = 3;
};

main.cpp

#include "data.h"

int
main()
{
    A a;
    B b;
    processData(a.info, A::len);
    processData(b.info, B::len);
}

You don't need any virtuals or templates here. Just add a SomeInfo* pointer and its length to Base, and provide a protected constructor to initialize them (and since there's no default constructor, it won't be possible to forget to initialize them).

The constructor being protected is not a hard requirement, but since Base is not an abstract base class anymore, making the constructor protected prevents Base from being instantiated.

class Base
{
public:
    struct SomeInfo
    {
        const char *name;
        const f32_t value;
    };

    void iterateInfo()
    {
        for (int i = 0; i < c_info_len; ++i) {
            DPRINTF("Name: %s - Value: %f \n", c_info[i].name,
                     c_info[i].value);
        }
    }

protected:
    explicit Base(const SomeInfo* info, int len) noexcept
        : c_info(info)
        , c_info_len(len)
    { }

private:
    const SomeInfo* c_info;
    int c_info_len;
};

class DerivedA : public Base
{
public:
    DerivedA() noexcept
        : Base(c_myInfo, sizeof(c_myInfo) / sizeof(c_myInfo[0]))
    { }

private:
    const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
};

class DerivedB : public Base
{
public:
    DerivedB() noexcept
        : Base(c_myInfo, sizeof(c_myInfo) / sizeof(c_myInfo[0]))
    { }

private:
    const SomeInfo c_myInfo[3] {
        {"NameB1", 2.1f},
        {"NameB2", 2.2f},
        {"NameB2", 2.3f}
    };
};

You can of course use a small, zero-overhead wrapper/adapter class instead of the c_info and c_info_len members in order to provide nicer and safer access (like begin() and end() support), but that's outside the scope of this answer.

As Peter Cordes pointed out, one issue with this approach is that the derived objects are now larger by the size of a pointer plus the size of an int if your final code still uses virtuals (virtual functions you haven't showed in your post.) If there's no virtuals anymore, then object size is only going to increase by an int. You did say that you're on a small embedded environment, so if a lot of these objects are going to be alive at the same time, then this might be something to worry about.

Peter also pointed out that since your c_myInfo arrays are const and use constant initializers, you might as well make them static. This will reduce the size of each derived object by the size of the array.


You could make Base a template and take the length of your const array. Something like this:

template<std::size_t Length>
class Base
{
  public:
    struct SomeInfo
    {
        const char *name;
        const float value;
    };

    const SomeInfo c_myInfo[Length];

    void iterateInfo()
    {
        //I would love to just write
        for(const auto& info : c_myInfo) {
            // work with info
        }
    }
};

And then initialize the array accordingly from each base class:

class DerivedA : public Base<2>
{
  public:
    DerivedA() : Base<2>{ SomeInfo{"NameA1", 1.1f}, {"NameA2", 1.2f} } {}
};

class DerivedB : public Base<3>
{
  public:
    DerivedB() : Base<3>{ SomeInfo{"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} } {}
};

And then use as you normally would. This method removes the polymorphism and uses no heap allocation (e.g. no std::vector), just as user SirNobbyNobbs requested.