为抽象基的一个数据成员获取多态行为的最佳实践 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
成为一个指针。现在我的问题开始:
- 首先,所有这一切是否是一个好方法,或者您在这里感觉到“代码味道”吗?像这样构建它是一种常见的做法吗?
假设基本方法没问题:
分配 BaseMember
指针的正确位置在哪里?在各种派生的构造函数中 classes?
我能否强制派生的 classes 实际上进行此分配?即,如果用户不理解或忘记他们需要分配一种或另一种 SomeDerivedMember
并使 BaseMember
指针指向它怎么办?在这种情况下如何强制它不编译?
这个成员应该在哪里释放(de-allocated)?我想 RAII 方法规定它将在分配的同一范围内(因此,派生的析构函数 class?)但这会强制库的每个用户记住执行此取消分配。相反,我可以在 Base
的析构函数中执行此操作(即在库中,而不是由用户执行)——但这会违反 RAII 原则吗?如果用户 DID 决定取消分配它(双重删除...)怎么办?
除此之外,您能想象一种甚至不使用动态分配也具有等效多态行为的方法吗?此代码适用于低级嵌入式 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 就是一个很好的例子)。
我想知道从软件设计的角度来看,对于每个派生的 class 应该具有不同类型的 多态性[=53= 的情况,什么是好的方法] 数据成员。更详细:
我正在编写一个库,它有一个抽象基础 class Base
,库的用户将从中继承。对于 Base
的一个成员,我们称它为 BaseMember
,我想要多态行为。我的意思是,从 Base
派生的各种 classes 将“包含”BaseMember
的不同子 classes - 有些将包含 OneDerivedMember
,其他的将包含 AnotherDerivedMember
等(所有这些都来自 BaseMember
,并且都在库中提供)。想要这样做的原因是我希望能够遍历 Base
指针的一些集合并激活 BaseMember
的一些功能(对于不同的派生 classes 实现不同).据我了解,我猜我必须使 BaseMember
成为一个指针。现在我的问题开始:
- 首先,所有这一切是否是一个好方法,或者您在这里感觉到“代码味道”吗?像这样构建它是一种常见的做法吗?
假设基本方法没问题:
分配
BaseMember
指针的正确位置在哪里?在各种派生的构造函数中 classes?我能否强制派生的 classes 实际上进行此分配?即,如果用户不理解或忘记他们需要分配一种或另一种
SomeDerivedMember
并使BaseMember
指针指向它怎么办?在这种情况下如何强制它不编译?这个成员应该在哪里释放(de-allocated)?我想 RAII 方法规定它将在分配的同一范围内(因此,派生的析构函数 class?)但这会强制库的每个用户记住执行此取消分配。相反,我可以在
Base
的析构函数中执行此操作(即在库中,而不是由用户执行)——但这会违反 RAII 原则吗?如果用户 DID 决定取消分配它(双重删除...)怎么办?除此之外,您能想象一种甚至不使用动态分配也具有等效多态行为的方法吗?此代码适用于低级嵌入式 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
编译时多态
这在标准库中被广泛使用,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 就是一个很好的例子)。