【问题标题】:Derive overloaded operator, but operate on same types only派生重载运算符,但仅对相同类型进行操作
【发布时间】:2012-09-26 09:38:41
【问题描述】:

假设我有一个基类和两个派生自它的类:

class Base
{
protected:
    double value;
public:
    virtual ~Base();

    Base(double value) : value(value) {}
    Base(const Base& B) { value=B.value; }

    Base operator+ (const Base& B) const { 
        return Base(value+B.value); 
    }

};

class final Derived1 : public Base {
public:
    Derived1(double value) : Base(value) {}
};

class final Derived2 : public Base {
public:
    Derived2(double value) : Base(value) {}
};

我要完成以下工作:

int main(int argc, char *argv[])
{
    Derived1 a = Derived1(4.0);
    Derived2 b = Derived2(3.0);

    a+a; // this should return a Derived1 object
    b+b; // this should return a Derived2 object

    a+b; // this should FAIL AT COMPILE TIME

    return 0;
}

换句话说,我想保证继承的operator+ 只对调用实例同类型的对象进行操作。

我如何干净地做到这一点?我发现自己为每个类重新定义了运算符:

class final Derived1 : public Base {
    ...
    Derived1 operator+ (const Derived1& D1) const {
        return Derived1(value+D1.value);
    }
    ...
};

class final Derived2 : public Base {
    ...            
    Derived2 operator+ (const Derived2& D1) const {
        return Derived2(value+D1.value);
    }
    ...
};

但这只是一种痛苦。此外,对我来说,重用代码似乎并不合适。

在这里使用的正确技术是什么?

【问题讨论】:

  • 如果你想要这个,不要把operator+放在底座上。 (另外,基地不应该有一个虚拟析构函数吗?)
  • @R.MartinhoFernandes:你是说我必须为每个派生类重新定义 operator+?但定义完全相同,除了类型。这里真的没有办法保存代码吗?
  • “完全一样,除了类型”不是“完全一样”。但是,是的,有一种方法可以保存代码。我会发布一个答案。
  • 但是您似乎没有在这里考虑一个大问题:如果我添加class Derived3 : public Derived1 会怎样? Derived3 d3; Derived1 d1; d1+d3; 应该编译吗?如果是,它应该有什么语义?如果没有,那你就不走运了。

标签: c++ oop class operator-overloading code-reuse


【解决方案1】:

如果您可以确保 Derived1Derived2 是叶类(即没有其他类可以从它们派生),您可以使用 curiously recurring template pattern 做到这一点:

template <typename T>
class BaseWithAddition : public Base {
    T operator+(T const& rhs) const {
        return T(value + rhs.value);
    }
};

class final Derived1 : public BaseWithAddition<Derived1> {
    // blah blah
};

class final Derived2 : public BaseWithAddition<Derived2> {
    // blah blah
};

final 是 C++11 的一项功能,可防止进一步推导。)

如果您允许从 Derived1Derived2 派生,那么您会遇到麻烦:

class Derived3 : public Derived1 {};
Derived3 d3;
Derived1 d1;
Derived1& d3_disguised = d3;
d1 + d3_disguised; // oooops, this is allowed

在编译时没有办法防止这种情况发生。即使你想允许它,如果没有multiple dispatch,这个操作也不容易获得体面的语义。

【讨论】:

  • +1:是的,这将是我对您评论的回复;派生类是最终的。快速测试表明这是可行的,但让我先了解一下这个模式。
  • 那么马上:这是否可取?这被认为是好的做法吗?
  • @RodyOldenhuis 是的,这很健康 :) 这不是“黑客”或任何东西。 CRTP 是最常用的模板习语之一。
  • 好吧,我今天学到了一些东西。谢谢!顺便说一句,您认为学习这些模式的最佳来源是什么?
  • @Rody 我是从C++ 模板:完整指南一书中学到的。有一本名为 "More C++ Idioms" 的 wikibook 包含很多很多成语,但我看到了一些非常具体或不值得推荐的内容。由于我还没有全部阅读,因此我无法保证,但如果您持怀疑态度,可能值得一看。
【解决方案2】:

只要value 仅在基类中定义,并且操作不需要访问任何派生成员,您就可以只定义基运算符并让隐式类型转换处理休息。对于不同类型的错误,使用基于枚举的系统来跟踪类型,然后做一个简单的比较来检查无效条件可能是值得的。

enum eTypeEnum {BASE, DER1, DER2};

class Base {
public:
  virtual ~Base(){}

  Base(double value) : eType(BASE),value(value) {}
  Base(const Base& B) { value=B.value; }

  Base operator+ (const Base& B) const {
    if (eType != B.eType) return -1; //error condition
    return Base(value+B.value);
  }
  double getVal(){return value;}
protected:
  eTypeEnum eType;
  double value;
};

class Derived1 : public Base {
public:
  Derived1(double value) : Base(value) {eType = DER1;}
};

class Derived2 : public Base {
public:
  Derived2(double value) : Base(value) {eType = DER2;}
};


int main() {
  int tmp;
  Derived1 a(4.0);
  Derived2 b(3.0);
  Base c(2.0);

  cout << "aa:" << (a+a).getVal();     // 8
  cout << "\nbb:" << (b+b).getVal();   // 6
  cout << "\nba:" << (b+a).getVal();   // 7
  cout << "\nab:"<< (a+b).getVal();    // 7

  cout << "\ncc:"<< (c+c).getVal();    // 4
  cout << "\nac:"<< (a+c).getVal();    // 6
  cout << "\nbc:" << (b+c).getVal();   // 5
  cout << "\nabc:" << (a+b+c).getVal();// 9
  cout << endl;
  cin >> tmp;
  return 0;
}

输出: AA:8 BB:6 巴:-1 ab:-1 抄送:4 交流:-1 公元前:-1 abc:1

我看到的唯一问题是,当将多个操作链接在一起时,强制转换会搞砸处理。在这里,a+b+c 432 的计算结果为 (a+b)+c,因此 a+b 位遇到错误情况(返回 -1),但被转换为 Base,这让 (-1)+c 返回“1”。

【讨论】:

  • 哦,错过了那部分。我更新了我的帖子,以反映添加了一个类型跟踪枚举系统,该系统允许基本运算符验证参数是否正确。一个漏洞是客户端可以在他们的构造函数中欺骗他们的类型,并且枚举需要随着新类的添加而更新,但这可能是你可以在不使用 RTTI 的情况下完成的最轻量级的代码。
  • 嗯...您返回的-1 被隐式转换为Base,一旦您将Base 的构造函数设为私有(我有),这将不再可能。此外,它仍然不是编译时错误,还有你已经指出的弱点。抱歉,我不赞成这个。
【解决方案3】:

您可以使用专门的模板函数来添加值。不幸的是,这个技巧不适用于运营商: 如果类型不同则失败,并返回正确的类型:

#include <type_traits>
class Base;
template <class Derived>
Derived add(const Derived& l, const Derived& r, 
            typename std::enable_if<std::is_base_of<Base,Derived>::value>::type* = NULL);


class Base
{
 ...
    template <class Derived>
    friend Derived add(const Derived& l, const Derived& r, 
       typename std::enable_if<std::is_base_of<Base,Derived>::value>::type* = NULL);
};

template <class Derived>
Derived add(const Derived& l, const Derived& r, 
 typename std::enable_if<std::is_base_of<Base,Derived>::value>::type* = NULL) 
{ 
    return l.value + r.value; 
}

它的工作证明:

int main() {
   int a = 0;
   a = a + a;
   Derived1 d11(0), d12(0);
   Derived2 d21(0), d22(0);
   add(d11, d12);
   add(d21, d22);
   add(d12, d22); // here it fails to compile...
}    

【讨论】:

  • +1:很好,但operator+ 现在是免费功能。这意味着如果我还有两个类Base2Base3,即使添加应该对那些 类起作用,添加这些实例也会失败; @R.MartinhoFernandes 的解决方案可以更好地处理封装问题。
  • 这个解决方案的一个问题是你已经覆盖了operator+。您已经为所有类定义了它,但它只对其中的一些类有用。
  • @Rody 我添加了 static_assert,但仍在考虑更好的解决方案。顺便说一句,这个 template operator + 不会被选择用于非模板运算符 + 存在的类型(如 int)。
  • @Rody 我改变了答案——我知道这不是你想要的——它是函数,而不是操作符——但它满足了所有其他要求
猜你喜欢
  • 2019-07-21
  • 1970-01-01
  • 2011-08-06
  • 1970-01-01
  • 2016-10-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多