【问题标题】:C++ Virtual Inheritance: static_cast "this" to virtual parent in initializer list of derivedC ++虚拟继承:static_cast“this”到派生的初始化列表中的虚拟父级
【发布时间】:2016-11-24 01:36:12
【问题描述】:

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

首先,您会看到这个示例代码 sn-p 并思考“为什么?”但相信我:这是有原因的。

代码如下:

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&lt;T*&gt;(this) 会导致分段错误......当使用英特尔C++ 编译器时。这已经在许多版本的 GCC、LLVM、MS Visual Studio 等上运行了多年。现在 ICPC 让它死了。

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

static_cast 放入构造函数主体(并更改其超级以匹配)使其避免此段错误。

所以我的问题是:规范在哪里说这个 [static cast] 是/不安全的?

【问题讨论】:

  • 您遇到了什么异常?这在我的系统上编译并运行良好(g++)
  • 详细说明 - 你这里的代码看起来完全没问题(虽然继承图花了我一段时间才画出^_^),所以我怀疑还有其他事情发生。此外,这个示例是否足以导致崩溃(例如,我是否应该期望它在运行时崩溃?)
  • 不清楚为什么需要所有这些虚拟继承——实际上并没有菱形的层次结构。有一个类在继承点阵中出现了两次 - LinkedListNode - 但这正是您虚拟继承的类,因此最终在MyClass 中有两个副本。跨度>
  • @curiousguy "[class.base.init]/16 可以为正在构建的对象调用成员函数(包括虚成员函数,10.3)。类似地,一个对象under construction 可以是 typeid 运算符 (5.2.8) 或 dynamic_cast (5.2.7) 的操作数。但是,如果这些操作在 ctor-initializer 中执行(或在从 ctor-initializer) 直接或间接调用的函数中,在基类的所有 mem-initializer 完成之前,操作的结果是未定义的。"但是 OP 的代码没有执行三个禁止的操作。
  • @iAdjunct 同样的例子还说D(this) 是UB。如果从E*A* 的演员阵容纯粹是静态的,那为什么会这样?在任何情况下,标准文本都没有在您试图做出的“动态”和“静态”之间做出区分。它描述了在什么条件下可以将派生指针转换为基指针。如果您想争辩本段不适用,您必须准确解释违反了哪些条件以及如何违反(特别是因为您自己要求提供语言律师的答案)。

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


【解决方案1】:

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

如果你坚持章节:

[expr.static.cast]/4 如果声明 @987654326,则表达式 e 可以使用 static_cast 形式的 static_cast&lt;T&gt;(e) 显式转换为类型 T @ 格式正确,对于某些发明的临时变量 t (8.5)。这种显式转换的效果与执行声明和初始化,然后使用临时变量作为转换的结果是一样的。

所以我们在InheritAndLinkList&lt;T&gt; 的构造函数中查看T t(this); 的有效性 - 直接初始化

[dcl.init]/17 ...

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

.

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


编辑

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

[class.cdtor]/3 显式或隐式地将引用X 类对象的指针(glvalue)转换为指向直接或间接基类的指针(引用) BXX 的构造及其直接或间接派生自 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),实际上比这更简单,因为您只在层次结构中向上转换了一步,因此无需关注中间类。

【讨论】:

  • 我同意它看起来像这样,我很确定就是这样。这个问题实际上源于它是一个虚拟基类。
  • 对象的曙光:在typeid和虚函数可以使用之前,在虚基类可以访问之后! GCC 为所有这些使用一个隐藏字段,即 vptr;可能是根本问题。
【解决方案2】:

可能是编译器错误

我对代码示例进行了简化和简化:

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)。

从实现的角度考虑,可以理解:对于非虚基类,派生到基类指针的转换是一个简单的“如果非空加一个固定偏移量”的调整,但是对于虚类来说,它涉及到一些更复杂的东西。基类,因为它们不驻留在固定的偏移量,根据定义(使基类虚拟很像添加一个间接级别)。

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

当输入构造函数的主体时(紧跟在{ 之后),正在构造的对象不会正式“存在”,因为它的生命周期尚未开始:如果构造函数主体以异常,对应的析构函数不会被调用。但是未开始的生命周期仍然具有“虚拟”对象的所有属性(= 具有虚拟功能的对象,函数或基类)。可以虚拟调用虚函数,调用当前类中的overrider,typeid表示构造中对象的类型等。

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

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

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

【讨论】:

  • 坦率地说,对我来说看起来像是一个编译器错误。它适用于 clang 和 msvc,仅适用于 gcc(我没有方便调试)。 [class.cdtor]/3 明确解决了这种精确情况,并且有一个与您的示例非常相似并声明它有效的示例。
  • 尽可能清晰:"[class.base.init]/15 [注意:因为 mem-initializer 在构造函数的范围内进行评估,this 指针可以在 mem-initializerexpression-list 中使用来引用正在被调用的对象已初始化。- end note]" 现在,这是一个非规范性注释,但很明显,标准的意图是允许这种用法。
  • @IgorTandetnik 如果这是一个错误,至少我有一个比问题中非常复杂的层次结构更好更简单的代码 sn-p。 ;)
  • 我改了答案,表示:可能是编译器的bug。
  • 你错过了一个关键点:它是一个虚拟基类。这非常重要。
猜你喜欢
  • 2016-03-04
  • 2015-11-09
  • 2013-06-12
  • 2020-10-26
  • 2015-07-13
  • 2012-08-31
  • 2013-02-26
  • 2015-09-09
相关资源
最近更新 更多