C++ 虚拟继承:在派生的初始值设定项列表中static_cast "this"虚拟父级

C++ Virtual Inheritance: static_cast "this" to virtual parent in initializer list of derived

本文关键字:虚拟 static 列表 cast this 派生 继承 C++      更新时间:2023-10-16

我有一些代码。它不起作用。

首先,你会看这个示例代码片段并思考"为什么?"但相信我:这是有原因的。

代码如下:

class LinkedListNode
// blaa
{
public:
    LinkedListNode ( void* p )
    {
        // blaa
    }
} ;
template <typename T>
class InheritAndLinkList
:   public virtual T
,   public LinkedListNode
{
public:
    InheritAndLinkList ()
    :    LinkedListNode ( static_cast<void*>(static_cast<T*>(this)) ) // an exception occurs here when ..... (scroll down)
    { }
} ;
template <typename T>
class Implements
:   public virtual InheritAndLinkList<T>
{ } ;

class    A
{
public:
    virtual void goA () =0 ;
} ;
class    B
:   public Implements<A>
{
public:
    virtual void goB () =0 ;
} ;

class    MyClass
:   public Implements<B>
{
public:
    virtual void goA ()
    {
        // blaa
    }
    virtual void goB ()
    {
        // blaa
    }
} ;

int main ( ... )
{
    MyClass * p = new MyClass () ; // ..... This line executes
    p->goA() ;
    p->goB() ;
    return 0 ;
}

具体的错误是,在构造时,表达式static_cast<T*>(this)会导致分割错误......使用英特尔C++编译器时。这已经在GCC,LLVM,MS Visual Studio等的许多版本上工作了多年。而现在ICPC让它死了。

我相信这是一件完全有效的事情。当这条线被调用时,T已经构造好了,应该可以使用...除非C++规范中还有其他奇怪的事情。

static_cast放在构造函数主体中(并更改其超级以匹配(会导致它避免这种段错误。

所以我的问题:规范中哪里说这个 [静态强制转换] 是/不安全的?

就其价值而言,代码对我来说看起来还可以。我认为这种static_cast的使用没有任何争议 - 它是普通的派生到基础指针转换。对我来说看起来像一个编译器错误。

如果你坚持章节和经文:

[expr.static.cast]/4 对于某些发明的临时变量t (8.5(,可以使用形式static_cast static_cast<T>(e)的表达式e T t(e);显式转换为T类型。此类显式转换的效果与执行声明和初始化,然后使用临时变量作为转换结果的效果相同。

因此,我们正在研究InheritAndLinkList<T>构造函数中T t(this);的有效性 - 直接初始化

[dcl.init]/17 ...

-- 否则,正在初始化的对象的初始值是初始值设定项表达式的(可能已转换(值。如有必要,将使用标准转换(第 4 条(将初始值设定项表达式转换为目标类型的 cv 非限定版本;不考虑用户定义的转换。

.

[conv.ptr]/3 类型为 "指向 cv D 的指针" 的 prvalue,其中 D 是类类型,可以转换为 "pointer "类型的 prvalue 到 cv B ",其中 BD 的基类(第 10 条(。如果B是不可访问的(条款11(或不明确的(10.2(基类D,那么需要这种转换的程序是格式不正确的。转换的结果是 指向派生类对象的基类子对象的指针。


编辑

经过评论中的激烈讨论,使用构造函数初始值设定项列表中的this并不那么简单 - 但我相信您的特定用途仍然是合法的。

[class.cdtor]/3 要显式或隐式地将引用类X对象的指针(glvalue(转换为指向X的直接或间接基类B的指针(引用(,X的构造及其直接或间接派生自B的所有直接或间接基础的构造应该已经开始,并且这些类的销毁尚未完成, 否则,转换会导致未定义的行为...[示例

struct A { };
struct B : virtual A { };
struct C : B { };
struct D : virtual A { D(A*); };
struct X { X(A*); };
struct E : C, D, X {
  E() : D(this), // undefined: upcast from E* to A*
                 // might use path E* ! D* ! A*
                 // but D is not constructed
                 // D((C*)this), // defined:
                 // E* ! C* defined because E() has started
                 // and C* ! A* defined because
                 // C fully constructed
  X(this) {      // defined: upon construction of X,
                 // C/B/D/A sublattice is fully constructed
  }
};

结束示例 ]

您的情况类似于上面示例中的X(this),实际上比这更简单,因为您只在层次结构中向上转换一个步骤,因此无需关注中间类。

这可能是一个编译器错误

我减少并简化了代码示例:

struct ctor_takes_int
{
    // dummy parameter needed to put expression in a ctor-init-list of derived class
    ctor_takes_int (int=0){ }
} ;
struct stupid_base
{
    //int nevermind;
} ;
struct upcast_in_init_list;
/* 
 * volatile = anti optimisation :
 * no value propagation possible on volatile variables
 * no constant propagation
 * no inlining of volatile pointer to function!
 */
int (*volatile upcast) (struct upcast_in_init_list *that);
struct upcast_in_init_list
: virtual stupid_base, ctor_takes_int
{
    upcast_in_init_list ()
    :    ctor_takes_int (upcast(this))
    { }
} ;
/*
 * volatile = anti optimisation
 * no dead assignment removal
 */
stupid_base *volatile p;
// must be compiled out of line
int do_upcast (upcast_in_init_list *that) {
    p = that;
    return 0;
}
int main ()
{
    upcast = &do_upcast;
    new upcast_in_init_list() ; 
    return 0 ;
}

程序在 http://www.tutorialspoint.com/compile_cpp11_online.php 崩溃

(请注意,使用 volatile 来防止某些优化,但在实践中似乎并不需要。它只是更"健壮",有一个"健壮的崩溃"。

如果我不是调用函数,而是在 ctor-init-list 中进行向上转换,则((p = this,0)) ,程序可以工作。这意味着编译器知道如何在构造函数对象的 ctor-init-list 内this上执行指针转换,但常见的转换代码不知道如何执行转换,因为派生对象此时不存在(例如,你不能使用它typeid(。

当您从实现的角度考虑它时,这是可以理解的:对于非虚拟基类,派生到基指针的转换是一个简单的"如果非 null 添加固定偏移量"调整,但它涉及虚拟基类更复杂的事情,因为它们不驻留在固定偏移量,根据定义(使基类成为虚拟很像添加间接级别(。

虚拟

项(虚函数、虚拟基类(的本质是对对象的动态(真实(类型的依赖。请注意,没有虚函数但具有虚拟基类的类在C++中不是"多态"的,并且不支持 RTTI(dynamic_casttypeid (,但仍必须具有一些"虚拟"运行时信息,vptr(vtable 指针(或一些虚拟基偏移量或指针。在任一情况下,运行时信息都会在构造期间初始化。

当进入构造函数的主体时(紧跟在{之后(,正在构造的对象不会正式"存在">,因为它的生存期尚未开始:如果构造函数主体退出并出现异常,则不会调用相应的析构函数。但是未启动的生存期仍然具有"虚拟"对象的所有属性(= 具有虚拟功能的对象,无论是函数还是基类(。虚拟函数可以虚拟调用,并且将调用当前类中的覆盖器,typeid将指示构造中对象的类型等。

在实践中,

在所有编译器中,向非虚拟基类的转换始终有效,因为不使用"虚拟"/动态信息,就像调用非虚拟函数"工作"(在实践中(在未构造的对象上一样,即使它不合法。

此外,初始化列表中this表达式(不是值(的转换也有效,因为它是一种特殊的优化情况:编译器知道类的布局和完整构造函数(用于构造完整对象的构造函数,而不是基类子对象(中所有虚拟的(静态(偏移量。您可以看到this是特殊情况:在完整构造函数的 ctor-init-list 中使用 (that = this, p = that, 0)(其中that是某个upcast_in_init_list *变量(不起作用,因为不再识别特殊。

处理this是一个基类构造函数调用(一个无法初始化虚拟基类的构造函数调用(显然也有效,我不知道为什么。