【问题标题】:typeid(*this).name() returns null pointer on delegated constructor of std::exception subclasstypeid(*this).name() 在 std::exception 子类的委托构造函数上返回空指针
【发布时间】:2022-11-02 17:50:43
【问题描述】:

我发现运行这个用 clang 编译的小 sn-p 代码有一个奇怪的行为:

#include <iostream>
#include <exception>
#include <typeinfo>

struct Foo : public std::exception {
    std::string myString;
    Foo(const std::string& str) : myString(str) {}
    Foo() : Foo(typeid(*this).name()) {}
};

int main()
{
    Foo f;
    std::cout << f.myString;
}

在委托构造函数中调用的指令typeid(*this).name() 返回一个导致分段错误的nullptr。在委托构造函数调用期间,std::exception 基类尚未初始化,这似乎是导致此行为的原因。

我想知道这段代码是否由于某种原因格式错误,或者这种行为是预期的。

我无法使用 g++ 重现此错误,其中代码运行良好。

它也仅在基类是 std::exception 时才会发生,在任何其他情况下,即使在 clang 上它也能正常工作。

【问题讨论】:

  • 这个错误有点道理。此时基类尚未初始化,因为尚未执行基构造函数。
  • @HolyBlackCat 奇怪的部分是它仅在基类为 std::exception 并且代码使用 clang++ 编译时发生,否则我无法重现它。
  • 我倾向于同意@HolyBlackCat。通常的 C++ 规则仍然适用。 *this 是一个指针解引用,它有一些常见的警告。如果该地址(还)没有对象,则只能以有限的方式使用生成的引用。 typeid 不是其中之一:它查询对象的动态类型,因此必须有一个对象。所以这个问题可能有点绕,但逻辑似乎很简单。未定义的行为,任何事情都可能发生,实际结果可能纯属巧合。
  • 任何具有虚拟成员的基类都会失败:godbolt.org/z/Kh4G3fYG3,MSVC 显示相同的行为。尽管即使使用非虚拟基础,它仍然是未定义的行为,它只是碰巧起作用
  • 如果我错了,请纠正我,但这意味着问题在于默认构造函数委托构造,对吗?因此,如果您将其更改为:Foo() : myString(typeid(*this).name()) {},那么基础将首先(默认)构建,并且定义明确。

标签: c++ exception inheritance language-lawyer clang++


【解决方案1】:

这具有未定义的行为。 typeid 允许应用于构造函数中正在构造的对象,也是成员初始化器列表,但只能在所有基类子对象的构造完成之后。见[class.base.init]/16

如果在构造基类之后使用typeid(*this),那么[class.cdtor]/5 将描述typeid 在这种特定情况下的行为。 (无论Foo 是否是派生最多的类型,它都会返回与Foo 对应的type_info。)


我认为这仅适用于*this 具有多态类型,即Foo 具有(继承的)虚拟成员函数(std::exception::whatstd::exception::~exception 在您的情况下)。当前的措辞似乎没有明确区分这一点,但对于非多态类型,甚至不应该评估表达式,因此*this 指的是正在构建的对象并不重要。有相关的开放 CWG 问题,例如CWG issue 1517

【讨论】:

    【解决方案2】:

    不管typeid(*this) 的语义如何,另一种选择是将更多的工作转移到编译时。具体来说:

    1. 实现一个 constexpr 函数,它可以获取类型的名称 - 我的意思是恰当的名称,而不是一些烦人的代码。你可以在this SO answer 中找到一个实现。
    2. 使用继承Foo 的类中的类型名称来构造您的Foo 基础。

      您可以使用 CRTP 和编写 class MyException : Foo&lt;MyException&gt; 进一步自动化此操作,使用 Foo 模板构造函数从其自己的类类型中提取模板参数类型。而这一切都将在编译时发生!

    【讨论】:

      【解决方案3】:

      UB 的原因是在构造第一个子对象之前使用该对象。 一般来说,内置的 RTTI 不能在构造函数中正确使用。虚函数早绑定;因为虚拟表只包含当前类信息;派生类尚未构造。我会将动态计算放在what 虚拟方法中:

      struct Foo : public std::exception {
          consteval Foo(char const * const str) : name{str} {};
          consteval Foo() = default;
          const char* what() const noexcept override {
              return name.empty()?
                     typeid(*this).name():
                     name;
          };
      private:
          std::string_view const name;
      };
      
      

      【讨论】:

        猜你喜欢
        • 2019-02-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多