【发布时间】: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{&derived_member.m_data}(或者将data转换为友元函数并调用base{data(derived_member)}) 不是,因为没有完成derived_member(也不是derived_member.m_data)的左值到右值(因此访问)
标签: c++ inheritance initialization language-lawyer constexpr