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&lt;ZZ&gt;(new ZZ(1)),例如。 @Gga:到第一点:我同意,这不像使用数字那么简单。当*,例如*y=*y0+(*x-*x0)/(*x1-*x0)*(*y1-*y0)(......虽然你几乎不会在有理数/整数上使用这个精确的公式,插值)。对于一个数字,语言为你提供了所有的东西。对于指针,它没有,因此您必须发明自己的定义,例如使用运算符重载:auto operator/(u.p.&lt;ZZ&gt; const&amp; a, u.p.&lt;ZZ&gt; const&amp; b) ... (其中 u.p. 表示唯一指针)

以上是关于C++:返回一个可以是特殊派生类的对象的主要内容,如果未能解决你的问题,请参考以下文章

C++中的派生类,可以不定义对象直接调用基类的成员和调用自己的成员函数嘛???

关于C++基类、派生类的引用和指针

大型程序的工具——多重继承与虚继承

总结C++基类与派生类的赋值兼容规则

生成一个派生类对象时,调用基类和派生类构造函数按啥次序

在 C++ 继承中,当指向基类的指针对象指向派生类时,不调用派生类析构函数