从基类

时间:2015-10-11 19:18:04

标签: c++ c++11 strict-aliasing

我有一个Result<T>模板类,其中包含一些error_typeT的联合。我想在基类中公开公共部分(错误)而不诉诸虚函数。

这是我的尝试:

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(假设这实际上是联合的当前动态类型)。

我认为这是便携式的吗?或者是否打破了一些指针别名规则?

编辑:我的问题是“这是便携式的”,但许多评论者对这里继承的使用感到困惑,所以我会澄清。

首先,这是一个玩具示例。请不要太过于字面意思或假设基地没有用。

设计有三个目标:

  1. 。错误和结果是互斥的,因此它们应该是联合的。
  2. 没有运行时开销。排除虚函数(另外,保持vtable指针与目标1冲突)。 RTTI也被排除在外。
  3. 均匀。通过同源指针或包装器,可以访问不同Result类型的公共字段。例如:如果我们讨论的是Result<T>而不是Future<T>,那么无论whenAny(FutureBase& a, FutureBase& b) / a具体类型如何,都应该可以b
  4. 如果愿意牺牲(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代码。

5 个答案:

答案 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_typeT都是标准布局。请注意,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_typeT必须是标准布局,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