如何消除这种与继承相关的代码气味?

时间:2019-06-17 08:23:24

标签: c++ c++11 inheritance

我需要用不同的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 个答案:

答案 0 :(得分: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}
    };
};

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

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

Peter还指出,由于您的int数组是c_myInfo 并且使用常量初始化程序,因此您最好将它们设为const。这样可以将每个派生对象的大小减小数组的大小。

答案 1 :(得分: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
        }
    }
};

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

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 所请求的一样。

答案 2 :(得分:8)

好吧,让我们简化所有不必要的并发症:)

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

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

答案 3 :(得分: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} };

};

答案 4 :(得分: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); }
};

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

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

现在我们可以谈谈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使用一个指针,使您的类型抽象,添加间接寻址,并为每个Derived类型添加一个全局vtable。

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

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

答案 5 :(得分: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} } };
};

答案 6 :(得分:4)

因此,如果您真的想按自己的方式组织数据,那么我就会明白为什么您会在现实生活中这么做:

使用C ++ 17的一种方法是返回代表内容列表的“视图”对象。然后可以在C ++ 11 for语句中使用它。您可以编写将start+len转换为视图的基本函数,因此无需将其添加到虚拟方法对象中。

创建与C ++ 11兼容的语句的视图对象并不难。另外,您可以考虑使用C ++ 98 for_each模板,这些模板可以采用开始和结束迭代器:您的开始迭代器为start;最终迭代器为start+len

答案 7 :(得分:3)

您可以将数据移动到类之外的二维数组中,并让每个类返回包含相关数据的索引。

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

答案 8 :(得分:3)

仅使虚函数直接返回对数据的引用(您需要更改为向量-否则不能使用大小不同的数组或C样式数组类型):

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

或如果指针是唯一可行的选择,则将其作为指针范围(如果可能,最好使用迭代器/范围适配器-对此有更多说明):

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

要使这最后一种方法与基于基于范围的for循环一起使用:一种方法是制作一个具有功能begin()/end()的小型“范围视图”类型-与{ {1}}

示例:

begin()/end()

然后用以下代码构造它:

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

如果不需要模板,很容易将其设置为非模板。