C++ - 让派生类从基类“继承”重载赋值运算符的安全/标准方法

Posted

技术标签:

【中文标题】C++ - 让派生类从基类“继承”重载赋值运算符的安全/标准方法【英文标题】:C++ - Safe/standard approach to having a derived class "inherit" the overloaded assignment operator from a base class 【发布时间】:2014-10-27 01:54:08 【问题描述】:

据我了解,在 C++ 中,派生类不会从基类继承重载赋值运算符。我在下面编写了一个示例,其中我为基类和派生类显式重载了赋值运算符。在底部的示例输出中,有一个部分:

Index:1 - Base Value:1
Index:1 - Derived Value:2
Index:2 - Base Value:2
Index:2 - Derived Value:2

预期的输出应该是:

Index:1 - Base Value:2
Index:1 - Derived Value:2
Index:2 - Base Value:2
Index:2 - Derived Value:2 

这个输出并不意外。我意识到赋值运算符不是继承的。派生类调用基类的赋值运算符是否有事实上的安全/标准方法?到目前为止,我最好的猜测是让派生类的赋值运算符函数将派生类对象向上转换为基类对象,并将 RHS 分配给 LHS。这是一种普遍接受的安全方法吗?

如果我使用“建议的解决方案”(页面底部),它可以工作,但这只是因为没有一个运算符被声明为virtual。如果我向上转换并使用非虚函数,则使用的函数版本取决于转换,而如果我使用虚函数,则使用的成员函数取决于对象的实际类型,而不是它是什么投射为。

谢谢。

代码清单


/*******************************************************************************
 * Preprocessor Directives
 ******************************************************************************/
#include <iostream>
using namespace std;


/*******************************************************************************
 * Class Declarations and Function Prototypes
 ******************************************************************************/
class Base 
   private:
   protected:
   public:
      int iBInt;
      Base();        /* Constructor */
      Base(int a);   /* Constructor - Set const member */
      Base & operator=(const Base& rhs);
      virtual ~Base();
;

class Derived : public Base 
   private:
   protected:
   public:
      int iDInt;
      Derived();        /* Constructor */
      Derived(int a);   /* Constructor - Set const member */
      Derived & operator=(const Derived& rhs);
      ~Derived();
;

/*******************************************************************************
 * Class and Function Definitions
 ******************************************************************************/
/******************************************************************************/
Base::Base(void) 
   cout << __FUNCTION__ << endl;
   iBInt = 0;
   cout << "iBInt: " << iBInt << endl;


/******************************************************************************/
Base::Base(int a) 
   cout << __FUNCTION__ << endl;
   iBInt = a;
   cout << "iBInt: " << iBInt << endl;


/******************************************************************************/
Base::~Base(void) 
   cout << __FUNCTION__ << endl;


/******************************************************************************/
Base& Base::operator=(const Base& rhs) 
   cout << "Base::" << __FUNCTION__ << endl;
   if (this == &rhs) 
      return *this;
   
   iBInt = rhs.iBInt;
   cout << "iBInt: " << iBInt << endl;
   return *this;


/******************************************************************************/
Derived::Derived(void) 
   cout << __FUNCTION__ << endl;
   iDInt = 0;
   cout << "iDInt: " << iDInt << endl;


/******************************************************************************/
Derived::Derived(int a) : Base(a) 
   cout << __FUNCTION__ << endl;
   iDInt = a;
   cout << "iDInt: " << iDInt << endl;


/******************************************************************************/
Derived::~Derived(void) 
   cout << __FUNCTION__ << endl;


/******************************************************************************/
Derived& Derived::operator=(const Derived& rhs) 
   cout << "Derived::" << __FUNCTION__ << endl;
   if (this == &rhs) 
      return *this;
   
   iDInt = rhs.iDInt;
   cout << "iDInt: " << iDInt << endl;
   return *this;



/*******************************************************************************
 * Main Entry Point
 ******************************************************************************/
int main(void) 
   int count = 3;
   /* Generate objects */
   Derived **bArr = new Derived*[count];
   for (int i=0; i<count; i++) 
      bArr[i] = new Derived(i);
   

   /* Set some values via overloaded assignment operator, and print out
    * updated values.
    */
   for (int i=0; i<count; i++) 
      cout << "Index:" << i << " - Base Value:" << bArr[i]->iBInt << endl;
      cout << "Index:" << i << " - Derived Value:" << bArr[i]->iDInt << endl;
   
   *bArr[1] = *bArr[2];
   for (int i=0; i<count; i++) 
      cout << "Index:" << i << " - Base Value:" << bArr[i]->iBInt << endl;
      cout << "Index:" << i << " - Derived Value:" << bArr[i]->iDInt << endl;
   
   /* Cleanup */
   for (int i=0; i<count; i++) 
      delete bArr[i];
   
   delete [] bArr;

   return 0;

样本输出


Base
iBInt: 0
Derived
iDInt: 0
Base
iBInt: 1
Derived
iDInt: 1
Base
iBInt: 2
Derived
iDInt: 2
Index:0 - Base Value:0
Index:0 - Derived Value:0
Index:1 - Base Value:1
Index:1 - Derived Value:1
Index:2 - Base Value:2
Index:2 - Derived Value:2
Derived::operator=
iDInt: 2
Index:0 - Base Value:0
Index:0 - Derived Value:0
Index:1 - Base Value:1
Index:1 - Derived Value:2
Index:2 - Base Value:2
Index:2 - Derived Value:2
~Derived
~Base
~Derived
~Base
~Derived
~Base

建议的解决方案/编辑


/******************************************************************************/
Derived& Derived::operator=(const Derived& rhs) 
   cout << "Derived::" << __FUNCTION__ << endl;
   if (this == &rhs) 
      return *this;
   
   Base* b1 = this;
   Base* b2 = (Base*)&rhs;
   *b1 = *b2;
   iDInt = rhs.iDInt;
   cout << "iDInt: " << iDInt << endl;
   return *this;

【问题讨论】:

不错的测试用例;干得好。 【参考方案1】:

我想说直接调用Base 赋值运算符:

Base::operator =(rhs);

以这种方式调用继承类的赋值运算符比使用指针体操调用它更简洁直接。


另一种方法是:

static_cast<Base &>(*this) = rhs;

这比使用指针调用运算符更干净,但仍然 (IMO) 比显式调用基运算符重载更不可读。

【讨论】:

在使用它时我应该记住任何“陷阱”(即:我是否应该覆盖 Base-from-Base,例如:Base &amp; Base::Base(const Base &amp;rhs) 构造函数)以确保始终使用它?我正在尝试限制分配/初始化发生的方式的数量,并确保它们都使用一个通用接口。 @Dogbert 此技术不需要复制构造函数;它显式地调用基类的赋值运算符。如果这不是你的问题,那么我不确定你在问什么。 @Dogbert 遵循rule of three -- 如果您需要定义自定义析构函数、复制构造函数或赋值运算符,那么您需要定义所有三个。通常,如果编译器生成的默认实现可以工作,则不应定义它们中的任何。 (在您提供的示例代码中,它们可以正常工作,尽管我知道这是一个人为的练习。) Derived d1 = Derived(a) 永远不会使用赋值运算符,它将始终使用复制构造函数。 (如果删除复制构造函数,此行将无法编译:error: use of deleted function ‘Derived::Derived(const Derived&amp;)’。)即使提供无参数构造函数和赋值运算符也是如此! 我应该注意到大多数编译器会忽略Derived d1 = Derived(a) 中的副本。然而,它们仍然需要一个可见的复制构造函数,因为省略是一种优化——您不会违反规范,因为您可以访问优化编译器。【参考方案2】:

是否有事实上的安全/标准方法来拥有派生类 调用基类的赋值运算符?

赋值运算符(以及一般的运算符)几乎总是暗示值语义。分配给字符串、向量、地图、日期或 int 是有意义的。

虚函数几乎总是暗示完全相反的。分配给流、窗口或数据库实现是没有意义的。几乎所有使用虚函数的类都应该禁用复制和复制赋值。

您遇到的所有问题都是由于明显想要混合难以混合且通常根本不应该混合的东西造成的。

我建议你禁用基类中的复制并将赋值运算符重命名为常规成员函数:

class Base : boost::noncopyable  // in C++11, use `= delete`
   public:
      int iBInt;
      Base();        /* Constructor */
      Base(int a);   /* Constructor - Set const member */
      virtual ~Base();
      virtual void Assign(Base const &other);
;

您几乎肯定会找到一个比Assign 更具描述性的名称,而且您很可能会发现一开始就调用函数“Assign”是完全错误的。

事实上,既然函数有了一个正常的名称,您就可以重新评估您的整个类设计,而不会被运算符语法“分心”。

可能的担忧:

在面向对象编程中,重写函数只是为了调用父实现通常被认为是不好的风格,因为这意味着基类没有充分建立不变量。

在基类虚函数中使用基类参数可能会导致非常复杂的设计,因为您实际上是在尝试使函数相对于两个参数成为虚拟函数。 “不可见”thisother 都可以是层次结构中的任何类。难道BaseDerived 的四种组合中的任何一种都需要做四种不同的事情吗?如果(应该预料到的)更多的派生类进入游戏怎么办?而且,这里的面向对象编程是否只是在源代码中散布类似的实现,而不是将它们集中在一个地方?

【讨论】:

是否可以禁用赋值运算符,或者您是否只需覆盖它并让它什么都不做(即:空定义主体)。 @Dogbert:您应该禁用它以及复制构造函数。在 C++11 之前,您可以通过将它们声明为私有而不是实现它们来实现(或者您只使用 boost::noncopyable)。在 C++11 中,您可以通过 = delete; 禁用这两者。

以上是关于C++ - 让派生类从基类“继承”重载赋值运算符的安全/标准方法的主要内容,如果未能解决你的问题,请参考以下文章

C++的探索路12继承与派生之高级篇--派生类与赋值运算符及多重继承

类的继承与派生

如何从基类调用派生赋值运算符?

C++派生类是不是可以从基类继承静态数据成员和静态成员函数?

<继承问题>可以把基类对象赋值给子类对象么?

派生类继承基类赋值运算符?