我有一个 Result<T>
包含一些 union 的模板类 error_type
和 T
.我想在不求助于虚函数的情况下公开基类中的公共(public)部分(错误)。
这是我的尝试:
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);
}
(这是精简的,如果不清楚,请参阅 extended version)。
基类利用标准布局在偏移量零处找到错误字段的地址。然后它将指针转换到 error_type
(假设这确实是 union 的当前动态类型)。
我认为这是可移植的是否正确?或者它是否违反了一些指针别名规则?
编辑:我的问题是“这是可移植的吗”,但许多评论者对这里使用继承感到困惑,所以我会澄清一下。
首先,这是一个玩具示例。请不要从字面上理解它或假设基类没有用。
设计有三个目标:
- 紧凑度。错误和结果是互斥的,所以它们应该在 union 中。
- 没有运行时开销。虚函数被排除在外(另外,持有虚表指针与目标 1 冲突)。 RTTI 也被排除在外。
- 一致性。不同的公共(public)字段
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;
};
如果我们牺牲 (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; };
};
同样,理由不相关。我只想确保原始示例符合 C++11 代码。
最佳答案
回答问题: 那是可移植的吗?
不可能
详细信息:
如果没有至少 type erasure,这是不可能的(不需要 RTTI/dynamic_cast,但至少需要一个虚函数)。已经有类型删除的工作解决方案 ( Boost.Any
)
原因如下:
你想实例化这个类
Result<int> r;
实例化模板类意味着允许编译器推断成员变量的大小,以便它可以在堆栈上分配对象。
但是在您的实现中:
private:
union { error_type mError; T mValue; };
你有一个变量 error_type
您似乎想以多态方式使用它。但是,如果您在模板实例化时修复了类型,您以后将无法更改它(不同的类型可能具有不同的大小!您也可以强加自己来修复对象的大小,但不要那样做。丑陋和骇人听闻)。
所以你有 2 个解决方案,使用虚函数,或者使用错误代码。
有可能做你想做的事,但你不能那样做:
Result<int> r;
r.setError(...);
具有您想要的确切界面。
有很多可能的解决方案,只要你允许虚函数和错误代码,为什么你不想在这里使用虚函数?如果性能很重要,请记住“设置”错误的成本与设置指向虚拟类的指针一样多(如果没有错误,则不需要解析 Vtable,无论如何模板代码中的 Vtable 是大多数时候可能会被优化掉)。
此外,如果您不想“分配”错误代码,您可以预先分配它们。
您可以执行以下操作:
template< typename Rtype>
class Result{
//... your detail here
~Result(){
if(error)
delete resultOrError.errorInstance;
else
delete resultOrError.resultValue;
}
private:
union {
bool error;
std::max_align_t mAligner;
};
union uif
{
Rtype * resultValue;
PointerToVirtualErrorHandler errorInstance;
} resultOrError;
}
您有 1 个结果类型,或 1 个指向具有所需错误的虚拟类的指针。您检查 bool 值以查看当前是否有错误或结果,然后您从 union 中获得相应的值。虚拟成本仅在出现错误时才支付,而对于常规结果,您只需支付 bool 检查的罚金。
当然,在上面的解决方案中,我使用了一个指向结果的指针,因为它允许通用结果,如果您对基本数据类型结果或仅具有基本数据类型的 POD 结构感兴趣,那么您也可以避免对结果使用指针。
注意在你的情况下std::exception_ptr
确实已经进行了类型删除,但是您丢失了一些类型信息,要再次获取丢失的类型信息,您可以自己实现类似于 std::exception_ptr
的东西但有足够的虚方法来允许安全地转换为正确的异常类型。
关于c++ - 从基类访问 union 的公共(public)部分,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33069102/