2014-08-28 113 views
5

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

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 
} 

分配的这种形式可能会导致inconcistent对象的状态,但没有编译器警告和代码看起来为非作恶对我来说是第一次:但是,如果通过引用发生转让,这是不正确的一目了然。

是否有任何成熟的习惯用法来检测此类问题?

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

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

我有两个数学概念,说ringfield。每个领域都是一个环,但不是相反。每个实现有几个实现,它们共享公共基类,即AbstractRingAbstractField,后者从前者派生。现在我尝试实现易于编写的基于std::shared_ptr的引用语义。所以我的Ring类包含一个std::shared_ptr<AbstractRing>持有它的实现,和一堆转发到该方法。我想写FieldRing继承,所以我不必重复这些方法。特定于某个字段的方法只需将指针投射到AbstractField,我想这样做是静态地进行投射。我可以确保指针在施工时实际上是AbstractField,但我担心有人会将Ring分配给Ring&,这实际上是Field,因此打破了我对所包含的共享指针的假定不变性。

+0

这里不是真正的问题,你有一个非抽象的基类吗? – 2014-08-28 11:01:29

+1

个人而言,我禁用了多态类型的复制构造函数和赋值运算符。继承基础多态性实际上不能很好地处理值类型。 – 2014-08-28 11:10:17

+0

@OliCharlesworth:我不明白。想想如何一个从按钮派生的切换按钮,我可以看到基类应该可实例化的情况,这个问题可能出现在现实世界中。因此我不会遵循任何“所有基础都必须抽象”的方法。如果这不是你想到的,那么请详细说明抽象基类如何能够帮助我解决问题。 – MvG 2014-08-28 11:10:21

回答

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; 
    } 
}; 

这可能是最明智的做法。

另一种方法是创建一个模板类,以便跟踪这个并简单地禁止使用基本指针*和引用&。模板化的解决方案可能会更难以正确实施,但可以进行静态类型检查来禁止沮丧。这是一个基本的版本,至少对于我来说,使用GCC 4.8和-std = C++ 11标志(对于static_assert),正确地给出了一个编译错误,“noDerivs(b)”是错误的来源。

#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; 
} 

如果用户首先创建了自己的引用并将其作为参数传递,则该特定模板仍可能被愚弄。最终,防止用户做愚蠢的事情是一种无望的努力。有时你只能做出公正的警告,并提供详细的最佳实践文档。

+0

有趣。 “在运行时无法检测到向下类型引用的分配”:在C++ 11中,可以在'Ring :: operator =(...)'实现内执行'typeid(* this)== typeid(Ring)' ,这是我想到的。不确定虚拟运营商是否有更好或更差的性能,将不得不在一天内进行测试。 “这是java中的标准分配方式。”但是Java有引用语义,所以我没有看到你在这里指的是什么类型的分配。阵列成员的分配接近,但仍然看起来不同于我。 Java方面可能不是主题,但我很好奇。 – MvG 2014-08-28 13:15:38

+0

@MvG typeid比较是在运行时使用RTTI完成的。虚拟分配使用类虚拟表并且不需要比较。一般来说,虚拟成员在C++中是首选,但我不确定RTTI是否会产生相当大的开销。虚拟呼叫是单向指针间接寻址,而RTTI可能涉及更多(完全不确定)。 我的java有点生疏。我相信我的意思是Object .equals和.clone,而不是赋值,但也涉及到类似的语义。 – Zoomulator 2014-08-28 13:25:43

+0

我刚刚意识到我写了运行时,当我的意思是编译时! – Zoomulator 2014-08-28 13:36:57