C ++虚拟继承:static_cast“this”到初始化列表中的虚拟父项派生

时间:2016-07-20 17:59:19

标签: c++ constructor language-lawyer lifetime virtual-inheritance

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

首先,您将看一下此示例代码段,并想一想“为什么?”但请相信我:有一个原因。

以下是代码:

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置于构造函数体中(并将其super更改为匹配)会导致它避免此seg-fault。

所以我的问题:在规范中哪里说 [静态演员] 是不安全的?

2 个答案:

答案 0 :(得分:4)

对于它的价值,代码看起来还不错。我在static_cast的使用中没有看到任何争议 - 它是一般的派生到基地指针转换。对我来说看起来像编译器错误。

如果你坚持章节和经文:

  

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

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

  

[dcl.init] / 17 ......

     

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

  

[conv.ptr] / 3 类型为“指向 cv D的指针”的prvalue,其中D是类类型,可以转换为“指针”类型的prvalue   到 cv B“,其中BD的基类(第10条)。如果B是不可访问的(第11条)或不明确的(10.2)基类D,则需要进行此转换的程序格式不正确。转换的结果是a   指向派生类对象的基类子对象的指针。

修改

在评论中进行了激烈的讨论之后,在构造函数初始化列表中使用this并不是那么简单 - 但我相信你的特定用途仍然合法。

  

[class.cdtor] / 3 显式或隐式地将引用类X的对象的指针(glvalue)转换为直接或间接的指针(引用) B的基类XX的构造以及直接或间接源自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),并且实际上比这更简单,因为您只在层次结构中强制转换一步,因此没有中间类需要关注。

答案 1 :(得分:0)

这可能是编译器错误

我缩小并简化了代码示例:

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来阻止一些优化,但在实践中似乎并不需要它。它只是更“强大”,以便“强烈崩溃”。)

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

当你在实现方面考虑它时,它是可以理解的:对于非虚拟基类,派生到基指针转换是一个简单的“if non null add a fixed offset”调整,但它涉及一些更复杂的虚拟根据定义,基类不存在于固定的偏移量中(使基类虚拟就像添加间接级别一样)。

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

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

实际上,在所有编译器中,转换到非虚拟基类总是有效的,因为没有使用“虚拟”/动态信息,就像调用非虚拟函数在实际中“工作”(在非构造对象上)一样,甚至如果不合法。

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

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