从基 class 访问联合的公共部分

Accessing common part of an union from base class

我有一个 Result<T> 模板 class,它包含一些 error_typeT 的联合。我想在基础 class 中公开公共部分(错误)而不求助于虚拟函数。

这是我的尝试:

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)。

基class利用标准布局在偏移量零处找到错误字段的地址。然后它将指针转换为 error_type(假设这确实是联合的当前动态类型)。

我认为这是便携式的是否正确?或者它是否违反了一些指针别名规则?


编辑:我的问题是 'is this portable',但是很多评论者对这里使用继承感到困惑,所以我会澄清一下。

首先,这是一个玩具示例。请不要从字面上理解它,也不要假设基数 class.

没有用

设计有三个目标:

  1. 紧凑度。 error 和 result 是互斥的,所以他们应该在一个联合中。
  2. 无运行时开销。虚函数被排除在外(另外,持有虚表指针与目标 1 冲突)。 RTTI 也排除在外。
  3. 均匀度。不同 Result 类型的公共字段应该可以通过同质指针或包装器访问。例如:如果我们谈论的不是 Result<T>,而是 Future<T>,那么无论 a / b 具体类型如何,都应该可以执行 whenAny(FutureBase& a, FutureBase& 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 代码。

抽象基础 class,错误和数据的两个实现,都具有多重继承,并使用 RTTI 或 is_valid() 成员在运行时判断它是哪个。

union {
    error_type mError;
    T mValue;
};

类型 T 不能保证与联合一起工作,例如它可能有一个非平凡的构造函数。关于联合和构造函数的一些信息:Initializing a union with a non-trivial constructor

回答问题: 那是便携式的吗?

不,这甚至不可能


详情:

如果没有至少 type erasure,这是 不可能的(不需要 RTTI/dynamic_cast,但至少需要一个虚函数)。已经有类型擦除的工作解决方案 (Boost.Any)

原因如下:

  • 您想实例化 class

    Result<int> r;

实例化模板class意味着允许编译器推断成员变量的大小,以便它可以在堆栈上分配对象。

但是在您的实施中:

private:
union { error_type mError; T mValue; };

您有一个变量 error_type,您似乎想以多态方式使用它。但是,如果您在模板实例化时修复了类型,您以后将无法更改它(不同的类型可能具有不同的大小!您也可以强加自己来修复对象的大小,但不要那样做。丑陋和骇人听闻)。

所以你有2个解决方案,使用虚函数,或者使用错误代码。

可以做你想做的事,但你不能那样做:

 Result<int> r;
 r.setError(...);

具有您想要的确切界面。

只要您允许虚函数和错误代码,就有许多可能的解决方案,为什么您不想在这里使用虚函数?如果性能很重要,请记住 "setting" 错误的成本与设置指向虚拟 class 的指针一样多(如果没有错误,则不需要解析 Vtable,并且无论如何,模板代码中的 Vtable 可能会在大多数时候被优化掉)。

此外,如果您不想 "allocate" 错误代码,您可以预先分配它们。

您可以执行以下操作:

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 个指向虚拟 class 的指针并出现所需错误。您检查布尔值以查看当前是否有错误或结果,然后您从联合中获得相应的值。仅当您出错时才会支付虚拟成本,而对于常规结果,您只会受到布尔检查的惩罚。

当然,在上面的解决方案中,我使用了指向结果的指针,因为它允许通用结果,如果您对基本数据类型结果或仅具有基本数据类型的 POD 结构感兴趣,那么您也可以避免对结果使用指针.

注意 在你的情况下 std::exception_ptr 确实已经输入了擦除,但是你丢失了一些类型信息,重新获得缺少类型信息,您可以自己实现类似于 std::exception_ptr 的东西,但具有足够的虚拟方法以允许安全地转换为正确的异常类型。

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(!)倍 - 所以,还不错......

另一个好处是您可能有 "cleaner" 设计并且没有 "aliasing"/"aligning" 问题。

如果您真的有某种称为紧凑性的理由(我不知道它是什么)- 通过这个非常简单的示例,您可以手动实现虚函数(但为什么???!!!)。你在这里:

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/memory 用法与 "virtual" 实现(惊喜)方面是相同的...

这是我自己尝试的一个严格关注可移植性的答案。

标准布局在§9.1[class.name]/7:

中定义

A standard-layout class is a class that:

  • has no non-static data members of type non-standard-layout class (or array of such types) or reference,
  • has no virtual functions (10.3) and no virtual base classes (10.1),
  • has the same access control (Clause 11) for all non-static data members,
  • has no non-standard-layout base classes,
  • either has no non-static data members in the most derived class and at most one base class with non-static data members, or has no base classes with non-static data members, and
  • has no base classes of the same type as the first non-static data member.

根据此定义 Result<T> 是标准布局,前提是:

  • error_typeT 都是标准布局。请注意,并非 std::exception_ptr 的保证,尽管在实践中可能如此。
  • T 不是 ResultBase

§9.2[class.mem]/20 指出:

A pointer to a standard-layout struct object, suitably converted using a reinterpret_cast, points to its initial member (or if that member is a bit-field, then to the unit in which it resides) and vice versa. [ Note: There might therefore be unnamed padding within a standard-layout struct object, but not at its beginning, as necessary to achieve appropriate alignment. —end note ]

这意味着空基 class 优化对于标准布局类型是强制性的。假设 Result<T> 确实有标准布局,ResultBase 中的 this 保证指向 Result<T>.

中的第一个字段

9.5[class.union]/1 状态:

In a union, at most one of the non-static data members can be active at any time, that is, the value of at most one of the non-static data members can be stored in a union at any time. [...] Each non-static data member is allocated as if it were the sole member of a struct.

另外§3.10[basic.lval]/10:

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined

  • the dynamic type of the object,
  • a cv-qualified version of the dynamic type of the object,
  • a type similar (as defined in 4.4) to the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to the dynamic type of the object,
  • a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its elements or nonstatic data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
  • a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
  • a char or unsigned char type.

这保证 reinterpret_cast<const error_type*>(this) 将产生指向 mError 字段的有效指针。

撇开所有争议不谈,这项技术看起来很便携。请记住形式限制:error_typeT 必须是标准布局,并且 T 可能不是类型 ResultBase.

旁注:在大多数编译器(至少是 GCC、Clang 和 MSVC)上,非标准布局类型也可以工作。只要 Result<T> 具有可预测的布局,错误和结果类型就无关紧要。