检测基类对指向派生类的引用的赋值

Detect assignment of base class to reference pointing at derived class

本文关键字:引用 赋值 派生 基类 检测      更新时间:2023-10-16

我目前正在研究多态类型和赋值操作之间的相互作用。我主要关心的是,是否有人试图将基类的值分配给派生类的对象,这会导致问题。

从这个答案中,我了解到基类的赋值运算符总是被派生类的隐含定义的赋值运算符所隐藏。因此,对于简单变量的赋值,不正确的类型会导致编译器错误。然而,如果通过引用进行分配,则情况并非如此:

class A { public: int a; };
class B : public A { public: int b; };
int main() {
  A a; a.a = 1;
  B b; b.a = 2; b.b = 3;
  // b = a; // good: won't compile
  A& c = b;
  c = a; // bad: inconcistent assignment
  return b.a*10 + b.b; // returns 13
}

这种形式的赋值可能会导致不一致的对象状态,但是没有编译器警告,而且代码乍一看并不邪恶。

有没有现成的习语来检测这些问题?

我想我只能希望运行时检测,如果我发现这样一个无效的赋值,就会抛出异常。我现在能想到的最好的方法是在基类中使用用户定义的赋值运算符,它使用运行时类型信息来确保this实际上是指向基类实例的指针,而不是指向派生类的指针,然后逐个成员手动复制。这听起来开销很大,严重影响了代码的可读性。有什么更容易的吗?

编辑:由于某些方法的适用性似乎取决于我想做什么,下面是一些细节。

我有两个数学概念,比如环和场。每个域都是一个环,但不是相反。每个都有几个实现,并且它们共享公共基类,即AbstractRingAbstractField,后者源自前者。现在,我尝试实现基于std::shared_ptr的易于编写的引用语义。因此,我的Ring类包含一个std::shared_ptr<AbstractRing>来保存它的实现,以及一堆转发给它的方法。我想把Field写成从Ring继承的,这样我就不必重复那些方法了。特定于字段的方法只需将指针强制转换为AbstractField,我希望静态地执行这种强制转换。我可以确保指针实际上是构造中的AbstractField,但我担心有人会将Ring分配给实际上是FieldRing&,从而打破我对包含的共享指针的假定不变量。

由于在编译时无法检测到向下转换类型引用的赋值,我建议使用动态解决方案。这是一种不寻常的情况,我通常会反对,但可能需要使用虚拟赋值运算符。

class Ring {
    virtual Ring& operator = ( const Ring& ring ) {
         /* Do ring assignment stuff. */
         return *this;
    }
};
class Field {
    virtual Ring& operator = ( const Ring& ring ) {
        /* Trying to assign a Ring to a Field. */
        throw someTypeError();
    }
    virtual Field& operator = ( const Field& field ) {
        /* Allow assignment of complete fields. */
        return *this;
    }
};

这可能是最明智的做法。

另一种选择可能是为引用创建一个模板类,它可以跟踪这一点,并简单地禁止使用基本指针*和引用&。模板化的解决方案可能更难正确实现,但会允许静态类型检查,从而禁止向下转换。这是一个基本版本,至少对我来说,它正确地给出了一个编译错误,"noDerivs(b)"是错误的根源,使用GCC 4.8和-std=c++11标志(用于static_assert)。

#include <type_traits>
template<class T>
struct CompleteRef {
    T& ref;
    template<class S>
    CompleteRef( S& ref ) : ref( ref ) {
        static_assert( std::is_same<T,S>::value, "Downcasting not allowed" );
        }
    T& get() const { return ref; }
    };
class A { int a; };
class B : public A { int b; };
void noDerivs( CompleteRef<A> a_ref ) {
    A& a = a_ref.get();
}
int main() {
    A a;
    B b;
    noDerivs( a );
    noDerivs( b );
    return 0;
}

如果用户首先创建自己的引用并将其作为参数传递,那么这个特定的模板仍然可能被愚弄。最后,保护用户不做愚蠢的事情是一种无望的努力。有时,你所能做的就是给出一个公平的警告,并提供一份详细的最佳实践文档。