How do I remove this inheritance-related code smell?

Sir*_*bbs 34 c++ inheritance c++11

I need to implement a lot of derived classes with different const member data. The data processing should be handled in the base class, but I can't find an elegant way to access the derived data. The code below is working, but I really don't like it.

The code needs to run in a small embedded environment so extensive usage of the heap or fancy libraries like Boost is no option.

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();
Run Code Online (Sandbox Code Playgroud)

Nik*_* C. 35

您在这里不需要任何虚拟或模板。只需在中添加一个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}
    };
};
Run Code Online (Sandbox Code Playgroud)

您当然可以使用小的零开销包装器/适配器类来代替c_infoand c_info_len成员,以提供更好,更安全的访问(如begin()end()支持),但这不在此答案的范围内。

正如Peter Cordes指出的那样,这种方法的一个问题是,int如果最终代码仍使用虚函数(您的帖子中未显示的虚函数),则派生的对象现在要比指针的大小加上an的大小大。如果不再有虚拟物体,则对象大小只会增加一个int。您确实说过您在一个小型嵌入式环境中,所以如果其中许多对象同时处于活动状态,则可能需要担心。

彼得还指出,由于您的c_myInfo数组是const 并且使用常量初始化器,因此您最好将它们设为static。这将通过数组的大小减少每个派生对象的大小。


Dei*_*Dei 13

您可以制作Base一个模板并获取const数组的长度。像这样:

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
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

然后从每个基类中相应地初始化数组:

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} } {}
};
Run Code Online (Sandbox Code Playgroud)

然后照常使用。此方法删除了多态性,并且不使用堆分配(例如no std::vector),就像用户SirNobbyNobbs所请求的那样。


Ada*_*ran 8

好吧,让我们简化所有不必要的复杂性:)

您的代码实际上可以归结为以下内容:

SomeInfo.h

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

void processData(const SomeInfo* c_myInfo, u8_t len);
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

数据

#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;
};
Run Code Online (Sandbox Code Playgroud)

main.cpp

#include "data.h"

int
main()
{
    A a;
    B b;
    processData(a.info, A::len);
    processData(b.info, B::len);
}
Run Code Online (Sandbox Code Playgroud)

  • 好吧,我可以想象。我只能建议使用组合而不是继承以及简单的功能。编码可以是一种简单而愉快的体验。我们只是出于某种原因使一切复杂化:) (6认同)
  • @SirNobbyNobbs“基类已经是几百行代码了”-这比简化示例可能突出显示的内容更像是一种代码味道, (3认同)
  • 在每个结构中没有u8_t len = 3;成员是没有意义的。`info []`数组成员的长度已经被静态称为派生类型的一部分。@SirNobbyNobbs:你可以做的是在A和B中分别有一个小的内联包装,将正确的args传递给一个通用的processData函数。如果需要,它可以是虚拟的,但是当您拥有完整的类型信息时,将其内联到每个呼叫站点中是很好的。(在派生类型函数上[[final`](https://en.cppreference.com/w/cpp/language/final)允许在更多情况下使用。) (3认同)

Oli*_*liv 7

您可以使用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} };

};
Run Code Online (Sandbox Code Playgroud)


Yak*_*ont 6

从词汇类型开始:

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); }
};
Run Code Online (Sandbox Code Playgroud)

该类型已std通过GSL 引入到C ++中(略有不同)。如果您还没有上述基本词汇类型,就足够了。

跨度代表已知长度的连续对象块的“指针”。

现在我们可以谈谈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
};
Run Code Online (Sandbox Code Playgroud)

现在,您的派生看起来像:

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

每个有两个指针的开销Base。一个vtable使用一个指针,使您的类型抽象,添加间接寻址,并为每个Derived类型添加一个全局vtable 。

现在,从理论上讲,您现在可以将开销降到数组的长度,并假定数组数据从之后开始Base,但这是脆弱的,不可移植的,并且仅在绝望时才有用。

尽管您可能对嵌入式代码中的模板不屑一顾(因为您应该进行任何类型的代码生成;但是代码生成意味着您可以从O(1)代码生成更多的O(1)二进制文件)。跨度词汇类型很紧凑,如果您的编译器设置相当激进,则应该内联为空。


SPD*_*SPD 5

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} } };
};
Run Code Online (Sandbox Code Playgroud)