检测基类对指向派生类的引用的分配

Posted

技术标签:

【中文标题】检测基类对指向派生类的引用的分配【英文标题】:Detect assignment of base class to reference pointing at derived class 【发布时间】:2014-10-22 05:07:24 【问题描述】:

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

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

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实际上是一个指向基类实例的指针,而不是派生类,然后进行逐个成员的手动复制。这听起来像很多开销,并且严重影响代码的可读性。有什么更简单的吗?

编辑:由于某些方法的适用性似乎取决于我想要做什么,这里有一些细节。

我有两个数学概念,比如ring 和field。每个领域都是一个环,但反过来不是。每个都有几个实现,它们共享公共基类,即AbstractRingAbstractField,后者派生自前者。现在我尝试基于std::shared_ptr 实现易于编写的引用语义。所以我的Ring 类包含一个std::shared_ptr<AbstractRing> 保存它的实现,以及一堆转发给它的方法。我想写Field 继承自Ring,所以我不必重复这些方法。特定于字段的方法只需将指针转换为AbstractField,我想静态地进行转换。我可以确保指针在构造时实际上是 AbstractField,但我担心有人会将 Ring 分配给实际上是 Ring&Field,从而打破了我假设的关于包含的共享的不变式指针。

【问题讨论】:

真正的问题不是你有一个非抽象的基类吗? 我个人禁用了多态类型的复制构造函数和赋值运算符。继承基多态性确实不能很好地与值类型配合使用。 @OliCharlesworth:我不知道怎么做。想想例如从按钮派生的切换按钮,我可以看到基类应该是可实例化的情况,并且问题可能在现实世界中出现。因此,我不会遵循任何“所有基础都必须是抽象的”方法。如果这不是您的想法,请详细说明抽象基类如何帮助解决我的情况。 @quantdev:现在,我没有绑定引用,我正在分配一个值。绑定是之前的行,C++ 不允许引用重新绑定。 也许你应该使用组合而不是继承?很难从给出的一般示例中分辨出来。 【参考方案1】:

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

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;

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

【讨论】:

有趣。 “在运行时无法检测到向下转换类型引用的赋值”:在 C++11 中,您可以在 Ring::operator=(…) 实现中执行 typeid(*this) == typeid(Ring),这就是我的想法。不确定虚拟算子的性能是好是坏,总有一天要测试一下。 “这是java中的标准赋值方式。”但是Java有引用语义,所以我看不出你在这里指的是什么类型的赋值。数组成员的分配很接近,但对我来说仍然看起来不同。 Java 方面可能是题外话,但我很好奇。 @MvG typeid 比较是在运行时使用 RTTI 完成的。虚拟分配使用类虚拟表,不需要比较。通常,虚拟成员在 C++ 中是首选,但我不确定 RTTI 相比之下是否会产生显着的开销。虚拟调用是单指针间接调用,而 RTTI 可能涉及更多(完全不确定)。我的java有点生锈了。我相信我的意思是 Object .equals 和 .clone 而不是赋值,但涉及到类似的语义。 我刚刚意识到我写了运行时,当我的意思是编译时! 根据cppreference on typeid,多态引用的typeid 描述了它的dynamic 类型,所以它也是在运行时。而且我希望编译器最终能够将 typeid 比较简化为所讨论的 vtable 的单个指针比较,这可能比指针间接更快,更重要的是,可以很好地与内联其余部分集成操作员。但所有这些都是理论上的,我只需要测试两者。 很抱歉我的回复有点乱。我的意思是 typeid 在运行时,但我的答案中的第一行现在是阅读关于类型推导的编译时间。实际上,除非您每秒对环和字段进行数千次分配,否则为什么不从句法的角度选择最适合您的呢?

以上是关于检测基类对指向派生类的引用的分配的主要内容,如果未能解决你的问题,请参考以下文章

基类对派生类指针的引用

在 C++ 中将派生类对象分配和访问到基类“指向指针”对象

声明指向基类和派生类的指针

为啥指向基类的派生类指针可以调用派生类成员函数? [复制]

5继承与派生3-类型兼容规则

派生类和基类的转换