【问题标题】:Can a constexpr constructor initialize a base class using the address of a member (GCC+MSVC vs Clang)?constexpr 构造函数可以使用成员的地址(GCC+MSVC vs Clang)初始化基类吗?
【发布时间】:2021-11-02 03:26:40
【问题描述】:

考虑以下代码:

struct base {
  int* base_member;
};

struct obj_t {
    constexpr int* data() {
        return &m_data;
    }
    int m_data;
};

class derived : public base {
public:
  constexpr derived() :
    base{derived_member.data()},
    derived_member{42}
  {}

private:
  obj_t derived_member;
};

constexpr derived g_obj{};

这里发生的是类derived 使用其成员derived_member 初始化其基类base。我通常会考虑这个 UB,因为基类在派生类成员之前初始化。因此,如果在常量表达式中使用它将是一个编译时错误。

但实际情况有所不同。虽然 GCC 和 MSVC 接受此代码,但 Clang 拒绝它 (godbolt)。问题是哪个编译器是正确的

第一个直觉支持 Clang 及其输出:

error: field 'derived_member' is uninitialized when used here [-Werror,-Wuninitialized]
error: constexpr variable 'g_obj' must be initialized by a constant expression
note: member call on object outside its lifetime is not allowed in a constant expression
...

请注意,Clang 会同时拒绝 -Wall -Wextra -Wpedantic -Werror-Wno-everything

另一方面,我有一个理论来解释为什么 GCC 和 MSVC 会接受这一点。问题是,derived_member 本身并没有被访问,只有它的地址在data() 成员函数中被获取。这个地址理论上是编译器知道的,不依赖于被初始化的对象。

这个理论的问题在于,在对象上调用了data()成员函数,这通常要求对象已经被构造。我想知道如果我使用& 运算符手动获取地址会发生什么。事实证明,在这种情况下,编译器三位一体一致接受代码(godbolt)。

我也使用std::addressof 进行测试。在这种情况下,所有三个编译器都接受代码 (godbolt)。

最后,我还测试了addressof 的粗略手动实现,如下所示:

template <typename T>
constexpr T* my_addressof(T& arg) noexcept {
    return &arg;
}

在这种情况下,所有三个编译器也都接受代码 (godbolt)。

因此,正确的行为是什么,哪个编译器/-s 是正确的?

编辑 1 - 背景信息

我会添加一些背景,以防有人感兴趣。我在处理一个名为PaSh 的项目时发现了这个问题。问题出现在我编写的一个文件中,该文件用于在 UNIX 函数 getopt_long 上创建 constexpr C++ 包装器。使用std::array 及其成员函数data 掩盖了该问题。损坏(Clang 不接受)版本的永久链接是 here。我在a recent commit 中修复了它,该版本在here 可用。

编辑 2 - 关于存储期限的说明

在看到@KitsuneSan 的答案后,我想澄清一下,我的问题是关于比第一个示例代码 sn-p 中给出的问题更普遍的问题。 g_obj 具有静态存储持续时间,因此 g_obj.derived_member.m_data 也具有它只是该示例的一个特征。在以下示例中,derived 类对象是临时的,因此 m_data 具有静态存储持续时间的参数不再适用:

struct base; // as above
struct obj_t; // as above
class derived; // as above

constexpr int dummy = [](){
    derived g_obj{};
    return 13;
}();

在这种情况下,代码被 GCC 和 MSVC 接受,而被 Clang 拒绝,就像原来的 (godbolt) 一样。

【问题讨论】:

  • 考虑 base{derived_member.data()} 是 UB,因为在对象的生命周期之外调用成员函数是 UB,但 base{&amp;derived_member.m_data}(或者将 data 转换为友元函数并调用 base{data(derived_member)} ) 不是,因为没有完成derived_member(也不是derived_member.m_data)的左值到右值(因此访问)

标签: c++ inheritance initialization language-lawyer constexpr


【解决方案1】:

根据标准第 7.7 节:

如果 E 满足核心常量表达式的约束,但是 E 的评估将评估具有未定义的操作 [library] 通过 [thread] 或调用中指定的行为 在 va_start 宏 ([cstdarg.syn]) 中,未指定 E 是否为 一个核心常量表达式。

基本上,编译器似乎允许拒绝触发 UB 的 constexpr 表达式,但不是必需。您给出的示例很棘手,因为有几层间接,所以我猜测编译器会丢失指向的内存是否已初始化(因为它至少是有效的 constexpr 存储)。

【讨论】:

  • 您提到的报价是针对图书馆 ub。关于语言 ub 的就是这个eel.is/c++draft/expr.const#5.7
  • 啊,你是对的,你引用的语言是作为对 C++11 wg21.cmeerw.net/cwg/issue1313> 的更正添加的。也许编译器作者从未完全修复它。该标准还在这里说:eel.is/c++draft/expr.const#11.2> 一个指针可以被认为是 constexpr,只要它指向一个具有静态持续时间的对象,在这种情况下是正确的,所以指针非常好,除非它被取消引用。
  • @KitsuneSan 我对问题进行了澄清,g_obj 具有静态存储持续时间这一事实是巧合,而不是问题试图解决的问题的一部分。当对象没有静态存储持续时间时也会出现此问题。
猜你喜欢
  • 2023-03-31
  • 2020-09-30
  • 1970-01-01
  • 1970-01-01
  • 2018-07-29
  • 2020-06-12
  • 2016-07-29
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多