【问题标题】:How to write operator= for anonymous union with non-trivial members with virtual methods如何使用具有虚拟方法的非平凡成员为匿名联合编写 operator=
【发布时间】:2018-10-18 18:50:15
【问题描述】:

C++11 使我们能够创建具有重要成员的匿名联合。这有时非常有用 - 例如,如果我想为一些没有默认 ctor 的非平凡对象创建 Holder 类。
让我们给这个 NonTrivial 对象添加一个虚拟方法,让它更有趣:

#include <stdint.h>
#include <stdio.h>

struct Base
{
    virtual void something() { printf("something\n"); }
};

struct NonTrivial : Base 
{
    explicit NonTrivial( int ) : a(1), b(2), c(3), d(4) { printf("NonTrivial\n"); }

    virtual void something() override { printf("something non trivial\n"); }

    int a;
    int b;
    int c;
    int d;
};

struct Holder
{
    Holder() : isNonTrivial(false), dummy(0x77) {}

    Holder( NonTrivial n) : isNonTrivial(true), nonTrivial( n ) {}

    bool isNonTrivial;
    union
    {
        int dummy;
        NonTrivial nonTrivial;
    };

    Holder & operator=( const Holder & rhs )
    {
        isNonTrivial = rhs.isNonTrivial;

        if( isNonTrivial ) 
            nonTrivial = rhs.nonTrivial;

        return *this;
    }
};

int main() {

    Holder holder_1;
    NonTrivial n(1);

    Holder holder_2( n );

    holder_1 = holder_2;

    holder_2.nonTrivial.something();
    holder_1.nonTrivial.something();

    return 0;

}

这行得通。但是,这有效,因为编译器实际上并没有在此处进行虚拟调用。让我们强迫它:

Base * ptr = &holder_1.nonTrivial;

ptr->something(); 

这会产生段错误。
但为什么?我或多或少做了一件显而易见的事情——检查持有人是否持有一个不平凡的对象,如果是的话——复制了它。
阅读程序集后,我看到这个operator= 实际上并没有从 rhs.nonTrivial 复制 vtable 指针。我认为这是因为 operator= for NonTrivial 应该只在完全构造的对象上调用,而完全构造的对象应该已经初始化了它的 vtable 指针 - 那么为什么还要麻烦和复制它呢?

问题:

  1. 我的想法正确吗?
  2. operator= 应该是什么样子 创建非平凡对象的完整副本?我有两个想法 - 删除 operator= 完全并强制用户使用复制 ctor - 或使用 放置新而不是nonTrivial = rhs.nonTrivial - 但也许 还有其他选择吗?

附:我知道 std::optional 之类的,我正在尝试自己了解如何做。

【问题讨论】:

  • operator= 无条件地复制 vptr b/c 参数是不合理的,因为参数 vptr 不一定属于该类。在一般情况下,检查 vptr 是否正确初始化的分支似乎很不幸。除非标准指定不允许这种行为......我怀疑这是一个错误,因为无论如何都没有指定 vtables。
  • @Lawrence 我在两个编译器上进行了测试,结果相似,所以我不认为这是一个错误。我想既然 Holder 按值而不是指针持有 NonTrivial,它必须是确切的类型,所以 vtables 必须相同。
  • 是的,确实存在指定行为被禁止的语言。请参阅en.cppreference.com/w/cpp/language/union:`如果联合的成员是具有用户定义的构造函数和析构函数的类,则要切换活动成员,通常需要显式析构函数和放置新的。在我相信的标准中可以找到类似的语言。
  • 您在一个从未构造过的对象上调用成员函数(即operator=()),其生命周期从未开始。当然,这是未定义的行为。
  • @IgorTandetnik 很公平。那么除了做一个placement new来构造对象没有别的办法了吗?

标签: c++ c++11 unions


【解决方案1】:

如果有人会偶然发现这个问题以寻找快速答案,以下是我使用 Placement new 解决此问题的方法:

template< typename T, typename ... Args >
void inplace_new( T & obj, Args && ... args )
{
    auto * t = &obj;

    t = new(t) T{ args... };
}

Holder & operator=( const Holder & rhs )
{
    isNonTrivial = rhs.isNonTrivial;

    if( isNonTrivial ) 
        inplace_new( nonTrivial, rhs.nonTrivial );

    return *this;
}

别忘了#include &lt;new&gt; :)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-05-28
    • 1970-01-01
    • 2015-08-10
    • 1970-01-01
    • 2019-10-21
    • 2011-01-18
    • 2018-10-28
    • 1970-01-01
    相关资源
    最近更新 更多