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

best practice for getting polymoprhic behavior for one data member of an abstract base class

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

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

  1. 首先,所有这一切是否是一个好方法,或者您在这里感觉到“代码味道”吗?像这样构建它是一种常见的做法吗?

假设基本方法没问题:

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

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

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

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

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

示例代码:

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

编辑按照 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 就是一个例子。部分行为委托给 class(称为特征)。 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();
    }
}

有一些限制,最值得注意的是:

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

另一方面,你会得到更好的 run-time 性能,一些计算可以在编译时以零 run-time 成本完成,你最终(通常)会处理 objects 和引用而不是指针。

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