【问题标题】:When is it safe to call this-> in constructor and destructor什么时候在构造函数和析构函数中调用 this-> 是安全的
【发布时间】:2015-07-27 07:51:07
【问题描述】:

到目前为止,我还没有找到确切的答案。什么时候从对象中调用this-> 是安全的。尤其是在构造函数和析构函数内部。

而且,当使用公共继承时。对 this 调用的结果使用向上和向下转换是否安全?

例如:

class foo
{
   foo():
   a(),
   b(this->a)//case 1
   {
       this-> a = 5; //case 2
   }

   int a;
   int b;
};

class bar: public baz
{
   bar():
   baz(this)//case 3 - assuming baz has a valid constructor
   {


   }

}

最后是最不可能的一个

foo()
   {
      if(static_cast<bar*>(this));//case 4
   }

以上哪些情况是合法的?

注意:我知道上面的很多做法都是不可取的。

【问题讨论】:

  • 您错误地使用了多态性。基类不应该依赖于孩子的结构。永远不要将 this 或 this 字段传递给基类的构造函数。
  • @avensus 这个字段,你是指类的任何字段,还是通过这个访问字段?
  • 我的意思是任何基类都不应该接收,因为它是初始化参数,也不是子字段的值,也不是子类的 this 指针本身。以这种 PHP 风格的方式,您可以在 base 和 child 之间提供强大的依赖关系。一个孩子已经依赖于基地。然后你添加反向依赖!顺便说一句,控制反转声明您的客户端代码仅依赖于基类,然后通过多态性,该基类依赖于真实的子实现。我的建议:只使用虚拟方法。不要重新发明多态性。
  • @avesus 是的,实际上你是对的。我现在想起来这样做也很不合逻辑。由于分配必须从构造函数完成。这意味着您将分配一个成员变量,然后依次使用该变量来初始化父类。我问这个问题的原因是因为我看到在 qt 中使用this 来检查一个类是否是特定其他类的父类。我将更新问题以包括背景:)
  • 不错!这个指针本身在任何时候都可以安全使用。成员按照声明的顺序进行初始化。但首先是基类。考虑这一点并编写您的安全代码。还要考虑这个指针值会有所不同。指针算术可以占据一席之地。我认为 Qt 使用了一些安全的技巧。如果您编写代码,程序员会回答 ;-)

标签: c++ c++11 constructor destructor


【解决方案1】:

在任何非静态成员函数中,this 指向调用该函数的对象。只要这是一个有效的对象,就可以安全使用。

在构造函数或析构函数的主体中,存在当前正在构造的类的有效对象。但是,如果这是某个派生类的基础子对象,那么此时只有基础子对象有效;因此,向下转换并尝试访问派生类的成员通常是不安全的。出于同样的原因,您需要在此处小心调用虚函数,因为它们是根据正在创建或销毁的类而不是最终覆盖器来调度的。

在构造函数的初始化列表中,您只需要注意访问已初始化的成员;也就是说,在当前正在初始化的成员之前声明的成员。

向上转换到基类总是安全的,因为基子对象总是首先初始化。

对于您刚刚添加到问题中的具体示例:

  • 案例 1 很好(如果很脆弱),因为此时 a 已被初始化。使用b 的值初始化a 将是未定义的,因为b 是在a 之后初始化的。
  • 案例 2 很好:此时所有成员都已初始化。
  • 案例 3 无法编译,因为没有合适的 foo 构造函数。如果有,那么这将取决于构造函数对它做了什么——它是否在初始化之前尝试访问成员。
  • 如果您添加缺少的),则案例 4 的格式正确,但如果您尝试使用指针访问该对象,则很危险。 this 尚未指向有效的 bar 对象(只有 foo 部分已初始化),因此访问 bar 的成员可能会产生未定义的行为。简单地检查指针是否为非空就可以了,并且总是会给出true(无论你是否应用了毫无意义的强制转换)。

【讨论】:

  • 好答案。但不是一个超级常见问题解答。您如何与超级常见问题解答竞争?案例 4 无法编译。如果你让它以明显的方式编译,它将返回一个(实际上,如果不是正式的)编译时常量真值,这使得将它放在 if 中似乎毫无意义。案例3也没有
  • @Yakk:我不知道超级常见问题解答可能是什么,并且无论如何也不想与之竞争。案例 4 中语法错误的好点;我只是阅读它以弄清楚意图,而没有编译它。我已经注意到 3 不会编译。但同样,问题的意图很明确,可以回答。
  • 当然:但是在这两种情况下,当你说“如果这段代码没有被破坏,它会做什么”给定其余代码时,你要么想出无操作,要么被破坏代码。如果 OP 想要检查一些非常具体的技术问题,他们不应该去把语法错误放在有问题的行上。代码问题区域中的“合法”代码问题取决于极其具体的技术问题。简短的回答:任何不能算括号的人都不应该这样做。 ;)
  • @Yakk:在这两种情况下,我都会描述如果您在代码中以这种方式使用this,通常会发生什么,以及示例的细节.如果您对问题的质量有任何抱怨,请抱怨问题,而不是答案。
  • @Yakk 我已经相应地改变了问题。如果您认为我还有什么可以改进的,请告诉我。我总是试图将我的代码减少到最少的问题,以尽可能简洁地说明我的观点。我对括号表示歉意,我希望问题的一般意义仍然很清楚:)。
【解决方案2】:

在 C++ super-faq 上有一个很好的条目:

https://isocpp.org/wiki/faq/ctors#using-this-in-ctors

有些人认为你不应该在构造函数中使用 this 指针 因为物体还没有完全成型。但是你可以使用这个 在构造函数中(在 {body} 甚至在初始化列表中) 如果你小心点。

这里有一些东西总是有效的:构造函数的 {body}(或 从构造函数调用的函数)可以可靠地访问数据 在基类中声明的成员和/或在 构造函数自己的类。这是因为所有这些数据成员 保证到时已完全建成 构造函数的 {body} 开始执行。

这是一个永远不会起作用的东西:构造函数的 {body}(或 从构造函数调用的函数)不能得到派生的 通过调用在 派生类。如果您的目标是在 派生类,你不会得到你想要的。请注意,您不会 获取派生类中的覆盖,与您的调用方式无关 虚成员函数:显式使用 this 指针(例如, this->method()),隐式使用 this 指针(例如,method()), 甚至调用其他调用虚拟成员的函数 在你的这个对象上运行。底线是:即使 调用者正在构造派生类的对象,在 基类的构造函数,你的对象还不是派生的 班级。您已收到警告。

以下是有时可行的方法:如果您传递任何数据 此对象中的成员到另一个数据成员的初始化程序,您必须 确保其他数据成员已经被初始化。这 好消息是您可以确定其他数据成员是否有 (或尚未)使用一些简单的语言进行初始化 独立于您正在使用的特定编译器的规则。 坏消息是你必须知道那些语言规则(例如,base 首先初始化类子对象(如果有,请查找顺序 多重和/或虚拟继承!),然后定义在 类按照它们出现的顺序进行初始化 类声明)。如果你不知道这些规则,那么不要通过任何 这个对象的数据成员(不管你是否 显式使用 this 关键字)到任何其他数据成员的 初始化器!如果你知道规则,请小心。

【讨论】:

    【解决方案3】:

    每个非静态成员函数都可以访问 this 指针...

    §9.3.2/1

    在非静态 (9.3) 成员函数的主体中,关键字this 是一个prvalue 表达式,其值是调用该函数的对象的地址。类 X 的成员函数中 this 的类型是 X*。如果成员函数声明为 const,则 this 的类型为 const X*,如果成员函数声明为 volatile,则 this 的类型为 volatile X*,如果成员函数声明为 const volatile,则 this 的类型为 const易失性 X*。

    ...其中构造函数和析构函数是成员函数...

    §12/1

    默认构造函数 (12.1)、复制构造函数和复制赋值运算符 (12.8)、移动构造函数和移动赋值运算符 (12.8) 和析构函数 (12.4) 是特殊的成员函数。

    ...不是静态的。

    §12.1/4

    构造函数不能是虚拟的 (10.3) 或静态的 (9.4)。

    §12.4/2

    析构函数不能是静态的。

    因此,this 在构造函数和析构函数中可用。但是有一些限制(尤其是在初始化列表中使用this)。

    (注意:在构造函数/析构函数体内部,所有子对象和成员的初始化都已完成,可以访问;见下文)。

    1.只能通过this 访问正在构造的对象(或其子对象)。

    §12.1/14

    在构造一个 const 对象期间,如果该对象或其任何子对象的值是通过不是从构造函数的this 指针直接或间接获得的泛左值访问的,则该对象的值或如此获得的子对象是未指定的。

    2。不要在基构造函数的派生类中调用被覆盖的虚函数

    §12.7/4

    成员函数,包括虚函数 (10.3),可以在构造或销毁 (12.6.2) 期间调用。当从构造函数或从析构函数直接或间接调用虚函数时,包括在类的非静态数据成员的构造或销毁期间,并且调用适用的对象是正在构造的对象(称为 x)或破坏,调用的函数是构造函数或析构函数类中的最终覆盖者,而不是在更派生的类中覆盖它。如果虚函数调用使用显式类成员访问 (5.2.5) 并且对象表达式引用 x 的完整对象或该对象的基类子对象之一,但不是 x 或其基类子对象之一,则行为未定义.

    3.不要应用dynamic_castthis 转换为除了正在构建的类型或其任何基本类型之外的任何类型。

    §12.7/6

    dynamic_casts (5.2.7) 可以在构造或销毁 (12.6.2) 期间使用。当 dynamic_cast 用于构造函数(包括非静态数据成员的 mem-initializer 或大括号或等式初始化器)或析构函数中,或用于从构造函数(直接或间接)调用的函数中或析构函数,如果 dynamic_cast 的操作数引用正在构造或销毁的对象,则该对象被认为是具有构造函数或析构函数类的类型的最派生对象。如果 dynamic_cast 的操作数指的是正在构造或销毁的对象,并且操作数的静态类型不是指向构造函数或析构函数自己的类或其基类之一的指针或对象,则 dynamic_cast 会导致未定义的行为。

    4.仅允许通过由构造的基类型组成的路径将this 转换为基类型指针。

    §12.7/3

    显式或隐式地将指向 X 类对象的指针(泛左值)转换为指向 X 的直接或间接基类 B 的指针(引用),X 的构造及其所有直接的构造从 B 直接或间接派生的或间接基类应已启动,并且这些类的销毁不应完成,否则转换会导致未定义的行为。要形成指向对象 obj 的直接非静态成员(或访问其值)的指针,obj 的构造应已开始且其销毁不应完成,否则指针值的计算(或访问该成员) value) 导致未定义的行为。

    访问初始化器列表和构造器主体中的子对象和成员

    原则上,您可以从初始化器列表中访问构造/初始化的对象,如果它们的初始化发生在访问它们之前。 初始化顺序为

    §12.6.2/10

    在非委托构造函数中,初始化按以下顺序进行:

    • 首先,并且仅对于最派生类 (1.8) 的构造函数,虚拟基类按照它们在基类的有向无环图的深度优先从左到右遍历中出现的顺序进行初始化,其中“从左到右”是派生类基说明符列表中基类的出现顺序。

    • 然后,直接基类按照它们出现在 base-specifier-list 中的声明顺序进行初始化(无论 mem-initializer 的顺序如何)。

    • 然后,非静态数据成员按照它们在类定义中声明的顺序进行初始化(同样与 mem-initializers 的顺序无关)。

    • 最后,构造函数体的复合语句被执行。

    【讨论】:

    • 谢谢!我想知道您是否可以扩展一下在 mem 初始化列表中使用 this 的限制?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-07
    • 2021-12-12
    • 1970-01-01
    • 2010-10-13
    相关资源
    最近更新 更多