微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

为抽象基类的一个数据成员获取多态行为的最佳实践

如何解决为抽象基类的一个数据成员获取多态行为的最佳实践

我想知道从软件设计的角度来看,对于每个派生类都应该具有不同类型的多态数据成员的情况,什么是好的方法。更详细:

我正在编写一个具有抽象基类 Base 的库,库的用户将从该类继承。对于 Base一个成员,我们称之为 BaseMember,我想要多态行为。我的意思是从 Base 派生的各种类将“包含”BaseMember 的不同子类 - 有些将包含 OneDerivedMember,其他将包含 AnotherDerivedMember 等(所有其中一些派生自 BaseMember,并且所有这些都在库中提供)。想要这样做的原因是,我希望能够查看一些 Base 指针集合并激活 BaseMember 的某些功能(对于其不同的派生类,它的实现方式不同)。据我了解,我猜我必须使 BaseMember 成为一个指针。现在我的问题开始了:

  1. 首先,所有这些是一种很好的方法还是您在这里感觉到“代码异味”?像这样构建它是一种常见的做法吗?

假设基本方法没问题:

  1. 分配 BaseMember 指针的正确位置在哪里?在各种派生类的构造函数中?

  2. 我可以强制派生类实际执行此分配吗?即,如果用户不理解或忘记他们需要分配一种或另一种 SomeDerivedMember 并使 BaseMember 指针指向它,该怎么办?在这种情况下如何强制它不编译?

  3. 该成员应该在哪里释放(解除分配)?我想 RAII 方法规定它将在它被分配的相同范围内(所以,派生类的析构函数?)但这迫使库的每个用户记住进行这种取消分配。相反,我可以在 Base 的析构函数中执行此操作(即在库中,而不是由用户执行) - 但这会违反 RAII 原则吗?如果用户 DID 决定取消分配它(双重删除...)怎么办?

  4. 除此之外,您能想象一种甚至不使用动态分配就具有等效多态行为的方法吗?此代码适用于低级嵌入式 MCU、Cortex M4 或类似内核和裸机(无操作系统) - 因此我尽量避免使用动态分配。

我觉得这种情况一定很常见,会有一种设计模式可以干净利落地解决这个问题,但我不确定那会是什么。

示例代码

#include <iostream>
#include <list>

using namespace std;

// --------------- Library.h ---------------
class BaseMember {
public:
  virtual void do_stuff() = 0;
};

class OneDerivedMember : public BaseMember {
  void do_stuff() {/* do stuff one way */}
};

class AnotherDerivedMember : public BaseMember {
  void do_stuff() {/* do stuff another way */}
};

class Base {
public:
  BaseMember* member;
  virtual ~Base() {/* delete member here or not? */}

};

// ------------- User of library ---------------
#include "Library.h"

class Derived : public Base {
public:
  Derived() {member = new OneDerivedMember;} // does it make sense to allocate member here?
  ~Derived() {delete member;} // delete here? or in Base?
};

class CluelessUserDerived : public Base {
public:
  CluelessUserDerived() {/* oh,I should have been allocating something here? didn't kNow */}
};

// I want to be able to do that sort of thing,which lead to the above (questionable?) design
int main() {
  list<Base*> my_list = {new Derived,new CluelessUserDerived};
  for (auto it = my_list.begin(); it != my_list.end(); it++) {
    (*it)->member->do_stuff();
  }
  return 0;
}

解决方法

EDIT 按照 OP 的建议,我将示例替换为完全可运行的示例

我会让界面难以被滥用:

#include <memory>
#include <list>
#include <iostream>

struct BaseMember 
{
    virtual void do_stuff() 
    { 
        std::cout << "BaseMember::do_stuff" << std::endl; 
    }
    virtual ~BaseMember() {}
};

//consider declaring these two classes final 
struct YourDefaulHere : BaseMember
{
    virtual void do_stuff() 
    { 
        std::cout << "YourDefaulHere::do_stuff" << std::endl; 
    };
    virtual ~YourDefaulHere() {}        
};

class WithSomeValue : public BaseMember
{
    double f;
public:
    WithSomeValue(double v) : f(v) {}
    virtual void do_stuff() 
    { 
        std::cout << "WithSomeValue::do_stuff " << f << std::endl; 
    };
    virtual ~WithSomeValue() {}               
};

class Base {
    std::unique_ptr<BaseMember> member;
public:
    explicit Base(std::unique_ptr<BaseMember> m) : member(std::move(m)) {}
    Base() : member(std::make_unique<YourDefaulHere>()) {}
    void do_stuff() { member->do_stuff(); }
    virtual ~Base() {}
};

//in the client code

class DerivedDefaulted : public Base
{
public:
    DerivedDefaulted() {}
};

class DerivedWithSomeValue : public Base
{
public:
    DerivedWithSomeValue(std::unique_ptr<BaseMember> m) : 
    Base(std::move(m)) {}
};

int main() {
    //consider using a smart pointer here
    std::list<Base*> my_list = {
        new DerivedDefaulted(),new DerivedWithSomeValue(std::make_unique<WithSomeValue>(5.0))
   };
   for (auto it = my_list.begin(); it != my_list.end(); it++) {
       (*it)->do_stuff();
   }
   return 0;
}

输出:

YourDefaulHere::do_stuff
WithSomeValue::do_stuff 5

你甚至可以提供一个工厂方法来创建 std::unique_ptr。 值得一提的是,只要接口中有复杂类型,就应该考虑库和客户端代码之间的二进制兼容性。

您还有两个选项可以在代码中引入多态行为。

传递一个函数

它可能不适合您的情况,但您可以简单地将 std::function 传入。这将减少 BaseMember 和 Base 之间的耦合。

编译时多态

这在标准库中被广泛使用,std::string 就是一个例子。部分行为委托给一个类(称为特征)。 https://en.cppreference.com/w/cpp/string/char_traits

Alexandrescu 的这本书详细介绍了这个想法 https://en.wikipedia.org/wiki/Modern_C%2B%2B_Design

这本书有些陈旧了,有些技术已经过时了,但它仍然是一本很棒的书。

这是一个解释这个想法的小例子:

#include <iostream>

struct Lock
{
    Lock() { std::cout << "Acquire lock" << std::endl; }
    ~Lock() { std::cout << "Release lock" << std::endl; }
};

struct NoAction {};

template<typename MultithreadPolicy>
struct Foo
{
    void somethingWithSharedResource()
    {
        MultithreadPolicy m;
        std::cout << "something here" << std::endl;
    }
};

typedef Foo<NoAction> NoThreadSafeFoo;
typedef Foo<Lock> LockingFoo;

int main()
{
    {
        NoThreadSafeFoo f;
        f.somethingWithSharedResource();
    }
    {
        LockingFoo f;
        f.somethingWithSharedResource();
    }
}

有一些限制,最明显的是:

  • 没有运行时插件,一切都必须在编译时知道
  • 您必须提供库的源代码(仅标头库)
  • 二进制文件大小和编译时间可能会增加

另一方面,您可以获得更好的运行时性能,一些计算可以在编译时完成,运行时成本为零,并且您最终(通常)将处理对象和引用而不是指针。

>

现代 C++ 肯定经常使用模板库(Boost 就是一个很好的例子)。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其他元素将获得点击?
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。)
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbcDriver发生异常。为什么?
这是用Java进行XML解析的最佳库。
Java的PriorityQueue的内置迭代器不会以任何特定顺序遍历数据结构。为什么?
如何在Java中聆听按键时移动图像。
Java“Program to an interface”。这是什么意思?