Как удалить запах кода, связанный с наследованием? - PullRequest
34 голосов
/ 17 июня 2019

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

Код должен работать в небольшой встроенной среде, поэтому широкое использование таких кучных или модных библиотек, как 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();

Ответы [ 9 ]

35 голосов
/ 17 июня 2019

Здесь вам не нужны виртуалы или шаблоны.Просто добавьте указатель 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.Это уменьшит размер каждого производного объекта на размер массива.

13 голосов
/ 17 июня 2019

Вы можете сделать 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 .

8 голосов
/ 17 июня 2019

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

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

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);
}
7 голосов
/ 17 июня 2019

Вы можете использовать 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} };

};
6 голосов
/ 17 июня 2019

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

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.Приведенного выше базового типа словаря достаточно, если у вас его еще нет.

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

Теперь мы можем поговорить оa 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 является компактным и не должен указывать ни на что, если настройки компилятора достаточно агрессивны.

5 голосов
/ 18 июня 2019

Как насчет 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} } };
};
4 голосов
/ 17 июня 2019

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

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

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

3 голосов
/ 17 июня 2019

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

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

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

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

Чтобы этот последний метод работал с на основе диапазона для цикла : один из способов заключается в создании небольшого типа '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)};
}

Легко сделать его не шаблонным, если шаблон нежелателен.

3 голосов
/ 17 июня 2019

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

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();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...