我有一个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);
}
(如果不清楚,可以查看extended version。
基类利用标准布局在偏移零处查找错误字段的地址。然后它将指针强制转换为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;
};
如果我们牺牲(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代码。
答案 0 :(得分:2)
回答这个问题: 那便携吗?
不,甚至不可能
<强>详细信息:强>
如果没有至少type erasure ,这是是不可能的(不需要RTTI / dynamic_cast,但至少需要一个虚函数)。已经有类型擦除的工作解决方案(Boost.Any
)
原因如下:
您想要实例化类
Result<int> r;
实例化模板类意味着允许编译器推导成员变量大小,以便它可以在堆栈上分配对象。
但是在您的实施中:
private:
union { error_type mError; T mValue; };
你有一个变量error_type
,似乎你想以多态方式使用它。但是,如果您在模板实例化时修改了类型,则以后无法更改它(不同的类型可能会有不同的大小!您也可以强迫自己修改对象的大小,但不要这样做。丑陋和hackish )。
所以你有2个解决方案,使用虚函数或使用错误代码。
可以做你想做的事,但你做不到:
Result<int> r;
r.setError(...);
使用您想要的确切界面。
只要您允许虚拟功能和错误代码,就有许多可能的解决方案,为什么您不想在这里使用虚拟功能?如果性能问题请记住&#34;设置&#34;错误与设置指向虚拟类的指针一样多(如果您没有错误,则不需要解析Vtable,并且无论如何,模板化代码中的Vtable可能会在大多数情况下被优化掉)。
此外,如果你不想&#34;分配&#34;错误代码,您可以预先分配它们。
您可以执行以下操作:
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个指向具有所需错误的虚拟类的指针。检查布尔值以查看当前是否有错误或结果,然后从联合中获取相应的值。仅当您有错误时才支付虚拟费用,而对于常规结果,您只需支付布尔检查的罚金。
当然在上面的解决方案中,我使用指向结果的指针,因为它允许通用结果,如果您对基本数据类型结果或只有基本数据类型的POD结构感兴趣,那么您可以避免使用指针也用于结果。 / p>
注意在您的情况下std::exception_ptr
已经输入了删除,但是您丢失了一些类型信息,以便再次获取您可以自己实现的缺失类型信息与std::exception_ptr
类似,但有足够的虚拟方法允许安全转换为正确的异常类型。
答案 1 :(得分:2)
C ++程序员常常错误地认为虚函数导致CPU和内存的使用率更高。即使我知道使用虚拟功能会耗费内存和CPU,但我称之为错误。但是,虚函数机制的手写替换在大多数情况下都是最糟糕的。
您已经说过如何使用虚拟功能实现目标 - 只需重复:
class ResultBase
{
public:
virtual ~ResultBase() {}
virtual bool hasError() const = 0;
virtual std::exception_ptr error() const = 0;
protected:
ResultBase() {}
};
及其实施:
template <class T>
class Result : public ResultBase
{
public:
Result(error_type error) { this->construct(error); }
Result2(T value) { this->construct(value); }
~Result(); // this does not change
bool hasError() const override { return mHasError; }
std::exception_ptr error() const override { return mData.mError; }
void setError(error_type error); // similar to your original approach
void setValue(T value); // similar to your original approach
private:
bool mHasError;
union Data
{
Data() {} // in this way you can use also Non-POD types
~Data() {}
error_type mError;
T mValue;
} mData;
void construct(error_type error)
{
mHasError = true;
new (&mData.mError) error_type(error);
}
void construct(T value)
{
mHasError = false;
new (&mData.mValue) T(value);
}
};
查看完整示例here。正如你所看到的那样,虚拟功能的版本要小3倍,快7倍! - 所以,不是那么糟糕......
另一个好处是你可能会更清洁&#34;设计并且没有&#34;别名&#34; /&#34;对齐&#34;问题。
如果你真的有一些称为紧凑的原因(我不知道它是什么) - 用这个非常简单的例子你可以手动实现虚函数(但为什么??? !!!)。你在这里:
class ResultBase;
struct ResultBaseVtable
{
bool (*hasError)(const ResultBase&);
error_type (*error)(const ResultBase&);
};
class ResultBase
{
public:
bool hasError() const { return vtable->hasError(*this); }
std::exception_ptr error() const { return vtable->error(*this); }
protected:
ResultBase(ResultBaseVtable* vtable) : vtable(vtable) {}
private:
ResultBaseVtable* vtable;
};
实施与先前版本相同,差异如下所示:
template <class T>
class Result : public ResultBase
{
public:
Result(error_type error) : ResultBase(&Result<T>::vtable)
{
this->construct(error);
}
Result(T value) : ResultBase(&Result<T>::vtable)
{
this->construct(value);
}
private:
static bool hasErrorVTable(const ResultBase& result)
{
return static_cast<const Result&>(result).hasError();
}
static error_type errorVTable(const ResultBase& result)
{
return static_cast<const Result&>(result).error();
}
static ResultBaseVtable vtable;
};
template <typename T>
ResultBaseVtable Result<T>::vtable{
&Result<T>::hasErrorVTable,
&Result<T>::errorVTable,
};
以上版本的CPU /内存使用情况与&#34;虚拟&#34;相同。实施(惊喜)......
答案 2 :(得分:2)
这是我自己尝试的答案,重点是可移植性。
标准布局在§9.1[class.name] / 7中定义:
标准布局类是一个类:
- 没有非标准布局类(或此类类型的数组)或引用类型的非静态数据成员,
- 没有虚函数(10.3),没有虚基类(10.1),
- 对所有非静态数据成员具有相同的访问控制(第11条),
- 没有非标准布局基类
- 在大多数派生类中没有非静态数据成员,并且最多只有一个具有非静态数据成员的基类,或者没有基础 具有非静态数据成员的类,以及
- 没有与第一个非静态数据成员相同类型的基类。
根据这个定义,Result<T>
是标准布局,条件是:
error_type
和T
都是标准布局。请注意,std::exception_ptr
保证不,但实际上可能会这样。T
不是ResultBase
。§9.2[class.mem] / 20表示:
指向标准布局结构对象的指针,适当地使用转换 reinterpret_cast指向其初始成员(或者如果该成员是 比特字段,然后到它所在的单元,反之亦然。 [ 注意:因此可能有一个未命名的填充 标准布局结构对象,但必要时不在其开头 实现适当的对齐。 - 后注]
这意味着标准布局类型必须使用空基类优化。假设Result<T>
确实具有标准布局,this
中的ResultBase
保证指向Result<T>
中的第一个字段。
9.5 [class.union] / 1州:
在联合中,最多一个非静态数据成员可以是活动的 在任何时候,也就是说,至多一个非静态数据的值 会员可以随时存储在工会中。 [...]每个非静态 数据成员被分配,就像它是结构的唯一成员一样。
另外§3.10[basic.lval] / 10:
如果程序试图通过访问对象的存储值 行为是除以下类型之一以外的glvalue 未定义
- 对象的动态类型,
- 对象的动态类型的cv限定版本,
- 与对象的动态类型相似的类型(如4.4中所定义)
- 与对象的动态类型对应的有符号或无符号类型的类型
- 与对象的动态类型的cv限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(包括, 递归地,子聚合的子元素或非静态数据成员 包含联盟),
- 一种类型,它是对象动态类型的(可能是cv限定的)基类类型,
- char或unsigned char类型。
这保证reinterpret_cast<const error_type*>(this)
将产生指向mError
字段的有效指针。
除了所有争议之外,这种技术看起来很便携。请记住正式的限制:error_type
和T
必须是标准布局,T
可能不是ResultBase
类型。
附注:在大多数编译器(至少是GCC,Clang和MSVC)上,非标准布局类型也可以使用。只要Result<T>
具有可预测的布局,错误和结果类型就无关紧要了。
答案 3 :(得分:1)
抽象基类,两个实现,用于错误和数据,都有多重继承,并使用RTTI或is_valid()
成员来告诉它在运行时它是什么。
答案 4 :(得分:1)
union {
error_type mError;
T mValue;
};
类型T不保证可以与联合使用,例如它可能有一个非平凡的构造函数。关于联盟和构造函数的一些信息:Initializing a union with a non-trivial constructor