Как удалить запах кода, связанный с наследованием?

Мне нужно реализовать много производных классов с различными данными константного члена. Обработка данных должна обрабатываться в базовом классе, но я не могу найти элегантный способ доступа к производным данным. Код ниже работает, но мне действительно это не нравится.

Код должен работать в небольшой встроенной среде, поэтому широкое использование динамических или модных библиотек, таких как Boost, невозможно.

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

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

        u8_t len = 0;
        const auto *returnedInfo = getDerivedInfo(len);
        for (int i = 0; i < len; i++)
        {
            DPRINTF("Name: %s - Value: %f \n", returnedInfo[i].name, returnedInfo[i].value);
        }
    }
    virtual const SomeInfo* getDerivedInfo(u8_t &length) = 0;
};

class DerivedA : public Base
{
  public:
    const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };

    virtual const SomeInfo* getDerivedInfo(u8_t &length) override
    {
        // Duplicated code in every derived implementation....
        length = sizeof(c_myInfo) / sizeof(c_myInfo[0]);
        return c_myInfo;
    }
};

class DerivedB : public Base
{
  public:
    const SomeInfo c_myInfo[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} };

    virtual const SomeInfo *getDerivedInfo(u8_t &length) override
    {
        // Duplicated code in every derived implementation....
        length = sizeof(c_myInfo) / sizeof(c_myInfo[0]);
        return c_myInfo;
    }
};

DerivedA instanceA;
DerivedB instanceB;
instanceA.iterateInfo();
instanceB.iterateInfo();

Ответ 1

Вам не нужны виртуалы или шаблоны здесь. Просто добавьте указатель SomeInfo* и его длину в Base, и предоставьте защищенный конструктор для их инициализации (и, поскольку нет конструктора по умолчанию, невозможно забыть инициализировать их).

Защищаемый конструктор не является жестким требованием, но поскольку Base больше не является абстрактным базовым классом, создание защищенного конструктора предотвращает создание экземпляра Base.

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

Конечно, вы можете использовать небольшой класс-оболочку/адаптер с нулевыми накладными расходами вместо членов c_info и c_info_len, чтобы обеспечить более удобный и безопасный доступ (например, поддержка begin() и end()), но который выходит за рамки этого ответа.,

Как отметил Питер Кордес, одна из проблем этого подхода заключается в том, что производные объекты теперь больше на размер указателя плюс размер int если в вашем конечном коде все еще используются виртуальные объекты (виртуальные функции, которые вы не показывали в своем посте). Если виртуалов больше нет, то размер объекта будет только увеличиваться на int. Вы сказали, что находитесь в небольшой встроенной среде, поэтому, если многие из этих объектов будут живы одновременно, то это может быть чем-то, о чем стоит беспокоиться.

Петр также отметил, что, поскольку ваши c_myInfo массивы const и использовать постоянные инициализаторы, вы можете также сделать их static. Это уменьшит размер каждого производного объекта на размер массива.

Ответ 2

Вы можете сделать Base шаблоном и взять длину вашего константного массива. Что-то вроде этого:

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

И затем инициализируйте массив соответственно из каждого базового класса:

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

А потом использовать как обычно. Этот метод удаляет полиморфизм и не использует выделение кучи (например, нет std::vector), как того требует пользователь SirNobbyNobbs.

Ответ 3

Ладно тогда давай упростим все ненужные осложнения :)

Ваш код действительно сводится к следующему:

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

Ответ 4

Вы можете использовать CRTP:

template<class Derived>
class impl_getDerivedInfo
  :public Base
{

    virtual const SomeInfo *getDerivedInfo(u8_t &length) override
    {
        //Duplicated code in every derived implementation....
        auto& self = static_cast<Derived&>(*this);
        length = sizeof(self.c_myInfo) / sizeof(self.c_myInfo[0]);
        return self.c_myInfo;
    }
};


class DerivedA : public impl_getDerivedInfo<DerivedA>
{
  public:
    const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
};

class DerivedB : public impl_getDerivedInfo<DerivedB>
{
  public:
    const SomeInfo c_myInfo[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} };

};

Ответ 5

Начните с типа словаря:

template<class T>
struct span {
  T* b = nullptr;
  T* e = nullptr;

  // these all do something reasonable:
  span()=default;
  span(span const&)=default;
  span& operator=(span const&)=default;

  // pair of pointers, or pointer and length:
  span( T* s, T* f ):b(s), e(f) {}
  span( T* s, size_t l ):span(s, s+l) {}

  // construct from an array of known length:
  template<size_t N>
  span( T(&arr)[N] ):span(arr, N) {}

  // Pointers are iterators:
  T* begin() const { return b; }
  T* end() const { return e; }

  // extended container-like utility functions:
  T* data() const { return begin(); }
  size_t size() const { return end()-begin(); }
  bool empty() const { return size()==0; }
  T& front() const { return *begin(); }
  T& back() const { return *(end()-1); }
};

// This is just here for the other array ctor,
// a span of const int can be constructed from
// an array of non-const int.
template<class T>
struct span<T const> {
  T const* b = nullptr;
  T const* e = nullptr;
  span( T const* s, T const* f ):b(s), e(f) {}
  span( T const* s, size_t l ):span(s, s+l) {}
  template<size_t N>
  span( T const(&arr)[N] ):span(arr, N) {}
  template<size_t N>
  span( T(&arr)[N] ):span(arr, N) {}
  T const* begin() const { return b; }
  T const* end() const { return e; }
  size_t size() const { return end()-begin(); }
  bool empty() const { return size()==0; }
  T const& front() const { return *begin(); }
  T const& back() const { return *(end()-1); }
};

этот тип был введен в C++ std (с небольшими отличиями) через GSL. Базовый тип словаря выше достаточно, если у вас его еще нет.

Пролет представляет "указатель" на блок смежных объектов известной длины.

Теперь мы можем поговорить о span<char>:

class Base
{
public:
  void iterateInfo()
  {
    for(const auto& info : c_mySpan) {
        DPRINTF("Name: %s - Value: %f \n", info.name, info.value);
    }
  }
private:
  span<const char> c_mySpan;
  Base( span<const char> s ):c_mySpan(s) {}
  Base(Base const&)=delete; // probably unsafe
};

Теперь ваш вывод выглядит так:

class DerivedA : public Base
{
public:
  const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
  DerivedA() : Base(c_myInfo) {}
};

Это накладные расходы двух указателей на Base. Vtable использует один указатель, делает ваш тип абстрактным, добавляет косвенность и добавляет один глобальный vtable для каждого Derived типа.

Теперь, теоретически, вы можете уменьшить это до длины массива и предположить, что данные массива начинаются сразу после Base, но это хрупко, непереносимо и полезно только в случае отчаяния.

В то время как вы можете быть совершенно осторожны с шаблонами во встроенном коде (как и любой другой тип генерации кода; генерация кода означает, что вы можете генерировать больше, чем O (1) двоичного кода из O (1) кода). Тип словаря span является компактным и не должен указывать ни на что, если настройки компилятора достаточно агрессивны.

Ответ 6

Поэтому, если вы действительно хотите, чтобы ваши данные были организованы так, как есть, и я понимаю, почему вы это делаете в реальной жизни:

Один из способов в С++ 17 - вернуть объект "представление", представляющий ваш список контента. Это может затем использоваться в С++ 11 for оператора. Вы можете написать базовую функцию, которая преобразует start+len в представление, так что вам не нужно добавлять в виртуальный метод cruft.

Не так сложно создать объект представления, совместимый с С++ 11 для оператора. В качестве альтернативы, вы можете использовать шаблоны С++ 98 for_each, которые могут использовать начальный и конечный итератор: ваш начальный итератор - start; конечный итератор - start+len.

Ответ 7

Как насчет CRTP + std :: array? Никаких дополнительных переменных, v-ptr или вызовов виртуальных функций. std :: array - очень тонкая оболочка для массива в стиле C. Оптимизация пустого базового класса гарантирует, что пространство не будет потрачено впустую. Это выглядит достаточно элегантно для меня :)

template<typename Derived>
class BaseT
{
  public:   
    struct SomeInfo
    {
        const char *name;
        const f32_t value;
    };

    void iterateInfo()
    {
        Derived* pDerived = static_cast<Derived*>(this);
        for (const auto& i: pDerived->c_myInfo)
        {
            printf("Name: %s - Value: %f \n", i.name, i.value);
        }
    }
};

class DerivedA : public BaseT<DerivedA>
{
  public:
    const std::array<SomeInfo,2> c_myInfo { { {"NameA1", 1.1f}, {"NameA2", 1.2f} } };
};

class DerivedB : public BaseT<DerivedB>
{
  public:
    const std::array<SomeInfo, 3> c_myInfo { { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} } };
};

Ответ 8

Вы можете переместить ваши данные в двумерный массив за пределы классов и заставить каждый класс возвращать индекс, который содержит соответствующие данные.

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

const vector<vector<SomeInfo>> masterStore{
    {{"NameA1", 1.1f}, {"NameA2", 1.2f}},
    {{"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f}}
    };

class Base
{
  public:
    void iterateInfo()
    {
        // I would love to just write
        // for(const auto& info : c_myInfo) {...}

        u8_t len = 0;
        auto index(getIndex());
        for(const auto& data : masterStore[index])
        {
            DPRINTF("Name: %s - Value: %f \n", data.name, data.value);
        }
    }
    virtual int getIndex() = 0;
};

class DerivedA : public Base
{
  public:

    int getIndex() override
    {
        return 0;
    }
};

class DerivedB : public Base
{
  public:

    int getIndex() override
    {
        return 1;
    }
};

DerivedA instanceA;
DerivedB instanceB;
instanceA.iterateInfo();
instanceB.iterateInfo();

Ответ 9

Просто заставьте виртуальную функцию возвращать ссылку на данные напрямую (тогда вам нужно перейти к вектору - это невозможно для массивов или типов массивов в стиле C с разными размерами):

virtual const std::vector<SomeInfo>& getDerivedInfo() = 0;

или, если указатели являются единственно возможным вариантом, в качестве диапазона указателя (итераторы/адаптер диапазона предпочтительнее, хотя, если это возможно, подробнее об этом):

virtual std::pair<SomeInfo*, SomeInfo*> getDerivedInfo() = 0;

Чтобы этот последний метод работал с циклом for на основе диапазона: один из способов - создать небольшой тип 'range view' с функциями begin()/end() - обязательно пару с begin()/end()

Пример:

template<class T>
struct ptr_range {
  std::pair<T*, T*> range_;
  auto begin(){return range_.first;}
  auto end(){return range_.second;}
};

Затем сконструируйте его с помощью:

virtual ptr_range<SomeInfo> getDerivedInfo() override
{
    return {std::begin(c_myInfo), std::end(c_myInfo)};
}

Это легко сделать без шаблона, если шаблон не нужен.