Val*_*lea 7 c++ strict-aliasing c++11
我有一个Result<T>
模板类,它包含一些error_type
和的联合T
.我想在不借助虚函数的情况下公开基类中的公共部分(错误).
这是我的尝试:
using error_type = std::exception_ptr;
struct ResultBase
{
error_type error() const
{
return *reinterpret_cast<const error_type*>(this);
}
protected:
ResultBase() { }
};
template <class T>
struct Result : ResultBase
{
Result() { new (&mError) error_type(); }
~Result() { mError.~error_type(); }
void setError(error_type error) { mError = error; }
private:
union { error_type mError; T mValue; };
};
static_assert(std::is_standard_layout<Result<int>>::value, "");
void check(bool condition) { if (!condition) std::terminate(); }
void f(const ResultBase& alias, Result<int>& r)
{
r.setError(std::make_exception_ptr(std::runtime_error("!")));
check(alias.error() != nullptr);
r.setError(std::exception_ptr());
check(alias.error() == nullptr);
}
int main()
{
Result<int> r;
f(r, r);
}
Run Code Online (Sandbox Code Playgroud)
(这是剥离的,如果不清楚,请参阅扩展版本).
基类利用标准布局来查找偏移零处的错误字段的地址.然后它将指针强制转换error_type
(假设这实际上是联合的当前动态类型).
我认为这是便携式的吗?或者是否打破了一些指针别名规则?
编辑:我的问题是'这是便携式的',但许多评论者对这里继承的使用感到困惑,所以我会澄清.
首先,这是一个玩具的例子.请不要太过于字面意思或假设基类没有用处.
该设计有三个目标:
Result
类型的公共字段应该通过同源指针或包装器来访问.例如:如果不是Result<T>
我们所说的Future<T>
,whenAny(FutureBase& a, FutureBase& b)
不管a
/ b
具体类型如何都应该可以做.如果愿意牺牲(1),这就变得微不足道了.就像是:
struct ResultBase
{
error_type mError;
};
template <class T>
struct Result : ResultBase
{
std::aligned_storage_t<sizeof(T), alignof(T)> mValue;
};
Run Code Online (Sandbox Code Playgroud)
如果我们牺牲(2)而不是目标(1),它可能看起来像这样:
struct ResultBase
{
virtual error_type error() const = 0;
};
template <class T>
struct Result : ResultBase
{
error_type error() const override { ... }
union { error_type mError; T mValue; };
};
Run Code Online (Sandbox Code Playgroud)
再次,理由是不相关的.我只是想确保原始样本符合C++ 11代码.
这是我自己尝试给出的答案,严格关注可移植性。
\n\n标准布局在 \xc2\xa79.1[class.name]/7 中定义:
\n\n\n\n\n标准布局类是这样的类:
\n\n\n
\n- 没有非标准布局类(或此类类型的数组)类型的非静态数据成员或引用,
\n- 没有虚函数 (10.3) 和虚基类 (10.1),
\n- 对所有非静态数据成员具有相同的访问控制(第 11 条),
\n- 没有非标准布局基类,
\n- 要么在最底层的派生类中没有非静态数据成员,并且最多有一个具有非静态数据成员的基类,要么没有具有非静态数据成员的基类,并且
\n- 没有与第一个非静态数据成员相同类型的基类。
\n
根据此定义,Result<T>
如果满足以下条件,则为标准布局:
error_type
都是T
标准布局。请注意,虽然在实践中可能会出现这种情况,但并不能保证这一点std::exception_ptr
。T
不是ResultBase
。\xc2\xa79.2[class.mem]/20 指出:
\n\n\n\n\n指向标准布局结构对象的指针,使用reinterpret_cast进行适当转换,指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然。[\n 注意:因此,标准布局结构对象中可能存在未命名的填充,但在其开头处则不然,这是实现适当对齐所必需的\n。\xe2\x80\x94结束注]
\n
这意味着空基类优化对于标准布局类型是强制性的。假设确实Result<T>
有标准布局,this
则 inResultBase
保证指向 中的第一个字段Result<T>
。
9.5[class.union]/1 规定:
\n\n\n\n\n在联合体中,任一时刻最多有一个非静态数据成员处于活动状态,即任一时刻联合体中最多可以存储一个非静态数据成员的值。[...] 每个非静态数据成员的分配就好像它是结构体的唯一成员一样。
\n
另外\xc2\xa73.10[basic.lval]/10:
\n\n\n\n\n如果程序尝试通过以下类型之一以外的\n泛左值访问对象的存储值,则行为\n未定义
\n\n\n
\n- 对象的动态类型,
\n- 对象动态类型的 cv 限定版本,
\n- 与对象的动态类型类似的类型(如 4.4 中定义),
\n- 与对象的动态类型相对应的有符号或无符号类型,
\n- 与对象动态类型的 cv 限定版本相对应的有符号或无符号类型,
\n- 聚合或联合类型,其元素或非静态数据成员中包含上述类型之一(递归地包括子聚合或包含的联合的元素或非静态数据成员),
\n- 是对象动态类型的(可能是 cv 限定的)基类类型的类型,
\n- char 或 unsigned char 类型。
\n
这保证reinterpret_cast<const error_type*>(this)
将产生一个指向该mError
字段的有效指针。
撇开所有争议不谈,这项技术看起来很便携。只需记住形式限制:error_type
并且T
必须是标准布局,并且T
可能不是类型ResultBase
。
旁注:在大多数编译器(至少 GCC、Clang 和 MSVC)上,非标准布局类型也可以工作。只要Result<T>
具有可预测的布局,错误和结果类型就无关紧要。