我可以从子类以外的另一个类调用抽象基类的公共赋值运算符吗?

Can I call abstract base class's public assignment operator from another class except subclass?

本文关键字:基类 赋值运算符 抽象 子类 我可以 另一个 调用      更新时间:2023-10-16

我遇到了MISRA C++ 2008年指南,本指南中的规则12-8-2说:

复制赋值运算符应在抽象类中声明为受保护或私有。

然后我想,当我公开抽象类的赋值运算符时,
是否可以从除其子类之外的另一个类调用它?
我认为这是不可能的。
如果这是真的,他们为什么要定义这个规则?

基本上,从类设计的角度来看,我不使用具有私有成员的抽象类,也不在基类中定义赋值运算符。因此,通常无需应用此规则。但是,如果存在抽象基类的公共赋值运算符,我会使其受到保护(如果可能的话,是私有的),因为公共是没有意义的。您知道应用此规则的其他充分理由吗?

我忽略了什么吗?

如果他们认为具有虚函数(非纯函数)的类是抽象的,那么最有可能防止切片。它的正常术语是基类。

#include <iostream>
struct A
{
  virtual ~A(){}
  virtual void foo(){ std::cout<<1<<std::endl; };
};
struct B : A
{
  virtual void foo(){ std::cout<<2<<std::endl; };
};
int main()
{
  B b;
  A a = b; // ops, wrong output because of slicing
}

但是,如果你的类真的很抽象(意味着它有纯虚方法),那么这个规则就没有意义了。

struct A
{
  virtual ~A(){}
  virtual void foo() = 0;
};
struct B : A
{
  virtual void foo(){}
};
int main()
{
  B b;
  A a = b; // compilation error
}

我不在基类中定义赋值运算符。

是否定义赋值运算符并不重要。如果您不这样做,编译器将为您生成一个。

抽象类是实现未知但你知道它们将如何表现或与其他类交互的类。 因此,您不太可能知道复制和赋值运算符中实际需要的抽象类的大小或其他细节。

主要问题还来自这篇文章的早期答案,讨论"切片问题",这在多态类中变得更加成问题,因为赋值运算符默认情况下不是虚拟的。

考虑一下

class A
{
};
class B : public A
{
}
int main()
{
B b1;
B b2;
A& a_ref = b2;
a_ref = b1;
}

现在在上面的情况下,a_ref 被初始化为 b2 对象,但在下一行中,当它分配给 b1 时,它将调用 A 的运算符 = 而不是 B 的运算符 =,这可能会改变其他对象 b2。 您可以想象在类 A 和 B 中不为空的情况。因此,规则是将复制构造函数和赋值运算符设置为私有而不是公共,如果您在抽象类中使赋值运算符公开,则使其成为虚拟,并在每个派生实现中使用dynamic_cast检查兼容性。

根据定义,抽象类不能被实例化(例如,见这里)。 无法实例化的类无法复制。 因此,这个规则是荒谬的。

我确认了@B Јовић和@NIRAJ RATHI指出的切片问题,
然后我注意到一种情况,即可以使用引用在抽象基类中调用公共赋值运算符。这是可能的,但可能会导致切片问题。因此,需要使赋值运算符成为虚拟的,在子类中覆盖它并向下转换。

#include <stdio.h>
class X {
public:
    int x;
    X(int inx): x(inx) {}
    virtual void doSomething() = 0;
    virtual void print() { printf("X(%d)n",x); }
    virtual X& operator=(const X& rhs) {
        printf("X& X::operator=() n");
        x = rhs.x;
        return *this;
    }
};
class Y : public X {
public:
    int y;
    Y(int inx,int iny) : X(inx), y(iny) {}
    void doSomething() { printf("Hi, I'm Y."); }
    virtual void print() { printf("Y(%d,%d)n",x,y); }  
    virtual X& operator=(const X& rhs);
};
X& Y::operator=(const X& rhs) {
    printf("X& Y::operator=() n");
    const Y& obj = dynamic_cast<const Y&>(rhs);
    X::operator=(rhs);
    y = obj.y;
    return *this;
}
int main()
{
    Y a(1,2);
    Y a2(3,4);
    X& r = a;
    r = a2; // calling assignment operator on ABC without slicing!!
    r.print();      
}

在我的结论中:
- 可以使用引用在抽象基类中调用赋值运算符。
- 规则 12-8-2 旨在防止切片问题。
- 如果没有切片问题,我不必总是应用规则 12-8-2。

BЈовић:但是,如果你的类真的很抽象(意味着它有纯虚方法),那么这个规则就没有意义了。

确实如此,因为赋值运算符可能会意外地被调用到基类的指针/引用,这总是会导致切片。

class cA
{
public:
    int x;
    virtual ~cA() { }
    virtual void f() = 0;
};
class cB: public cA
{
    int y;
    virtual void f() { }
};
cA * a = new cB, * b = new cB;
*a = *b; // slicing: x is copied, y is not

该规则确实有意义,原因如下:

  • 防止部分分配
  • 防止在不兼容的类之间进行复制

假设我们有一个抽象的基类Animal和几个派生类,LizardChicken

class Animal {
public:
  ...
};
class Lizard: public Animal {
  ...
};
class Chicken: public Animal {
  ...
};

默认情况下,赋值运算符是公共的。现在考虑以下代码:

Lizard liz1;
Lizard liz2;
Animal *pAnimal1 = &liz1;
Animal *pAnimal2 = &liz2;
...
*pAnimal1 = *pAnimal2;

在最后一行调用的赋值运算符是 Animal 类的赋值运算符,即使所涉及的对象是 Lizard 类型。因此,只会修改 liz1 的Animal部分。这是部分作业。分配后,liz1 的Animal成员具有他们从 liz2 获得的值,但 liz1 的Lizard成员保持不变。

也可以通过Animal指针将Lizard分配给Chicken。这没有多大意义,因为它们彼此不兼容,我们可能希望防止这样的事情发生。防止此类分配的最简单方法是在Animal中保护operator=。这样,可以将蜥蜴分配给蜥蜴,也可以将鸡分配给鸡,但禁止部分和混合类型的分配:

class Animal {
protected:
  Animal& operator=(const Animal& rhs);
  ...
};

或者,如果此类层次结构中禁止复制,则可以=delete定义赋值运算符。实际上,私有或受保护的赋值运算符根本不需要返回*this。他们可以返回类型 void。