C++:返回一个可以是特殊派生类的对象
Posted
技术标签:
【中文标题】C++:返回一个可以是特殊派生类的对象【英文标题】:C++ : return an object that can be of a special derived class 【发布时间】:2020-08-26 19:21:15 【问题描述】:对不起,篇幅较长,但我不知道如何在没有介绍的情况下提出这个问题。
让我们假设必须实现一个“数字”(class CNum
的对象),它可以是(有符号)整数 (∈ℤ) 或有理数 (∈ℚ)。
想象一下有一个成员函数prn
来打印数字。当然,我们希望以不同的方式打印整数和有理数。
一个典型的“old style”(C/ish)实现可能是这样的:
class CNum
public:
char type; // In this example, 'Q' or 'Z'.
[...]
char *prn(char *s)
if(type=='Z') sprintf(s,"something...."); else
if(type=='Q') sprintf(s,"something else ...");
return s;
[...]
;
更多“现代”(C++/ish)方法倾向于使用派生类和虚函数。 所以我会写这样的:
class CNum
public:
[...]
virtual char *prn(char *s) ....;
;
class ZZ : public CNum
long int n; // n is the value.
// implementation for integers, with proper prn()
;
class QQ : public CNum
long int a; unsigned long int b; // a/b is the value.
// implementation for rationals, with proper prn()
;
...而且看起来好多了(不是吗?)。
好的...现在,我需要将一个整数除以另一个整数的代码。 一般来说,两个整数之间的比率是有理数,所以我会写:
QQ operator / (ZZ &x, ZZ &y)
QQ R;
long int j;
if(y.n<0) R.a=-x.n; R.b=-y.n;
else R.a= x.n; R.b= y.n;
for(j=2; j<=abs(R.a) && j<=R.b; j++) while(!(R.a%j) && !(R.b%j)) R.a/=j; R.b/=j;
if(R.b==1) ; // (it's a whole number, stored as rational)(sorry...)
return R;
最长的行 (for(j...
) 允许,例如,得到 2/3 而不是 2006/3009 的结果
那么,真正的问题来了:在if(R.b==1)
的情况下,如何返回class ZZ
类型的对象(而不是class QQ
)?
在“old style”方法中这将是微不足道的。但这意味着所有成员函数都应该是 if(type==...)... else if(type==...)... else ...
的聚合——不是我想要维护的。
专家会有什么建议?
(我找到了一个返回对新对象的引用的解决方案,但它比问题更糟糕,否则它会泄漏内存)
另外:如果有人理解了这个问题(同样不知道答案):对更好的标题有什么建议吗?
【问题讨论】:
返回某种智能指针? 【参考方案1】:根据 SO 的建议,避免在 cmets 中回答问题。但我没有提供完整的答案......所以这是介于评论和答案之间的某个地方。
你面临的问题是QQ和ZZ是不同大小的不同对象。值得注意的是,其中一个有一个额外的成员变量(unsigned long int b)。
因此运算符 / 不能根据上下文按值返回,因为它们不兼容。它们有不同的类型,但也有不同的大小。
基本上这里的解决方案是类型擦除。本质上,您想创建一个类型,可以说是 MyNumber。它是一个容器,在其中存储类型 MyInteger 或 MyRational。
这意味着对 MyNumber 执行的每个操作都必须进行运行时测试,以查看它是否在其中存储了 MyInteger 或 MyRational 的实例......然后调用适当的函数来对该类型进行操作。
要实现类型擦除,您有两个基本选项:
在 MyNumber 内部,它将存储指向 MyInteger 或 MyRational 的指针。这增加了一个额外的间接层,并且可能会命中您的缓存,因此对于像数字类型这样的东西可能是不可取的。
在编译时已知所有可能的 MyNumber 表示形式的情况下使用 Union。这使它们可以存储在堆栈中,一般来说,您可以更轻松地确保一系列 MyNumber 实例中包含的数据靠近在一起。
一个快速的谷歌搜索出现了这个,其中有一个很好的类型擦除示例: https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Type_Erasure
该实现在内部使用 unique_ptr,因此它采用分配路线。您可能需要查找工会并了解如何使用它们。
编辑:
很明显,自从我完成 C++ 以来已经太久了!你可以使用 std::variant 作为你的联合,而不是做一个联合,这将是一堆开始深入研究你可能不熟悉的语言使用的东西。
应该是这样的:
class MyNumber
public:
MyNumber( MyInteger i )...
MyNumber( MyRational r )...
MyNumber operator / (MyNumber const& other) const
... internally use the DivideVisitor struct with std::visit ...
private:
struct DivideVisitor
... look up how std::visit works with std::variant ...
... and the implementation for this will become clear ...
;
std::variant< MyInteger, MyRational > m_internal;
;
【讨论】:
据我了解,这些是“旧样式方法”主题的变体,在第二种情况下,行为切换逻辑在“访问”中。对?还是我错过了什么重要的事情? 嗯,我认为对于您正在寻找的任何解决方案,有 3 件事非常重要。首先,您需要将其包装为一个行为类似于值类型的单一类型 - 这样您就可以像为浮点或其他类型编写代码一样。其次,要像您希望编译器这样优化的基本数学类型。因此,如果您在编译时执行一系列已知的操作,则该解决方案应该使编译器能够尽可能优化所有这些代码。最后,数据位置对于数学类型很重要,以提高缓存效率。【参考方案2】:您正在询问任何强类型语言的常见问题。问题是,函数结果,特别是它的类型是在编译时指定的,但函数参数只有在运行时才知道。为了解决这个问题,你必须使用多态性。
您称之为“旧 C 风格”的解决方案转换为 C++ 如下:
std::unique_ptr<CNum> operator/ (ZZ const& x, ZZ const& y)
//calculate "result = x/y"
//if rational
return std::make_unique<QQ>(result);
//else if whole number
return std::make_unique<ZZ>(result);
这样,您返回一个指向有理数或整数的唯一指针——然后您可以在进一步的计算中使用它,也可以使用CNum
。
此外,如果您想知道它到底是哪一个,您可以使用dynamic_cast
:
ZZ a;
ZZ b;
auto res = a/b;
if(QQ* d = dynamic_cast<QQ*>(res.get()))
//result is rational
else if(ZZ* d = dynamic_cast<ZZ*>(res.get()))
//result is whole
但是,您应该很少需要它。
此外,还有其他选择。例如,您可以实现一个虚函数 std::string type()
,它为派生的有理类返回 "Q"
,为整数返回 "Z"
,从而替换 C 风格解决方案中的 char type
变量。
总结:旧的 C 风格几乎没有比现代 C++ 风格更好。
【讨论】:
感谢这个例子。它似乎与我尝试的类似,但是(据我所知)返回的值不能用作数字:它更像是int* operator / (int, int)
,即两个数字的比率不会返回结果:它返回一个指向结果的指针,而不是......不是吗?如果所有指令都像p=n/m;
(→修改为p=*(n/m);
)会很好...但是对于像简单插值y=y0+(x-x0)/(x1-x0)*(y1-y0)
这样更长的表达式会很混乱...
一个可悲的细节是make_unique
是 C++14,而我有 C++11 的“Dev-C++”,并且(直到今天早上)我对它很满意.我的问题,我知道...解决方法似乎是#define __cplusplus 201104L
,但我不知道它是否有任何灾难性后果。
@GGa:关于 C++11 限制:make_unique
只是用于调用构造函数并将其包装在唯一指针中的语法糖。没有它,你可以简单地写std::unique_ptr<ZZ>(new ZZ(1))
,例如。
@Gga:到第一点:我同意,这不像使用数字那么简单。当*,例如*y=*y0+(*x-*x0)/(*x1-*x0)*(*y1-*y0)
(......虽然你几乎不会在有理数/整数上使用这个精确的公式,插值)。对于一个数字,语言为你提供了所有的东西。对于指针,它没有,因此您必须发明自己的定义,例如使用运算符重载:auto operator/(u.p.<ZZ> const& a, u.p.<ZZ> const& b) ...
(其中 u.p. 表示唯一指针)以上是关于C++:返回一个可以是特殊派生类的对象的主要内容,如果未能解决你的问题,请参考以下文章