2016-06-28 17 views
2

我想下面的数据结构转换:如何复制一个不平凡的c + +联盟?

template<typename ValueT, typename ChildT> 
class MyUnion 
{ 
public: 
    MyUnion() : mChild(NULL) {} 
private: 
    union { 
     ChildT* mChild; 
     ValueT* mValue; 
    }; 
}; 

ValueT既可以是POD(intfloat等)和非平凡的东西像Vec3std::string这是它最初实现的原因一个指向动态分配内存的指针。但是,使用C++ 11,我们现在可以直接在类中存储值。我在寻找的结果是这样的:

template<typename ValueT, typename ChildT> 
class MyUnion 
{ 
public: 
    MyUnion() : mChild(NULL) {} 
private: 
    union { 
     ChildT* mChild; 
     ValueT mValue; 
    }; 
}; 

改变,这使得编译器抱怨拷贝构造函数丢失,所以我想实现

MyUnion(const MyUnion& other); 
MyUnion& operator=(const MyUnion& other); 

和理想,此举也构造。以前,编译器为我实现了这些。有了POD,我可以做一个memcpy或类似的东西 - 我现在可以使用相同的东西并期待正确的结果吗?

+1

不,一般来说,你可能不会使用memcpy。正是这样一种类似'std :: string'的类“非平凡”(不是简单地可复制的,确切地说)。您需要一些方法来了解哪些联合成员当前处于活动状态,并相应地编写您的复制构造函数。或者,使用['boost :: variant'](http://www.boost.org/doc/libs/1_61_0/doc/html/variant.html) –

+0

'ValueT'也需要复制&移动构造函数/赋值/等等。它会使这个任务变得简单很多,因为只要MyUnion是可以复制构建和移动构造的。即使POD可以使用初始化副本和“std :: move”afaik –

+0

@XerenNarcy问题不在于如何复制成员,就像知道要复制哪个成员一样。 –

回答

2

首先,如果mValue是一个指向动态分配内存的指针,那么这个类的默认拷贝构造函数是非常不安全的,除非你很乐意泄漏内存。

因为,哪个副本负责删除对象?它们看起来完全相同,并且没有共享指针。所以我认为你只是泄露了它。 (也许你有一些“经理”班?但是那么你不会问如​​何在工会现在存储它的价值,你会吗?因此,tsk tsk泄漏:p)

在大多数情况下,你想要存储一个额外的标志,告诉你哪个成员正在被初始化。然后它被称为“歧视联盟”,因为有实际的信息可以用来区分它所处的两种状态中的哪一种。

我会给出一个最小版本,假设ValueT是可复制和移动的。

template<typename ValueT, typename ChildT> 
class MyUnion 
{ 
    public: 
    // Accessors, with ref qualifiers. 
    bool have_value() const { return mHaveValue; } 
    ValueT & get_value() & { return mValue; } 
    ValueT && get_value() && { return std::move(mValue); } 
    ValueT const & get_value() const & { return mValue; } 
    ChildT * & get_child() & { return mChild; } 
    ChildT * && get_child() && { return mChild; } 
    ChildT * const & get_child() const & { return mChild; } 

    // Constructors. Default, copy, and move. 

    MyUnion() { 
     this->init_child(nullptr); 
    } 

    MyUnion(const MyUnion & other) { 
     if (other.have_value()) { 
     this->init_value(other.get_value()); 
     } else { 
     this->init_child(other.get_child()); 
     } 
    } 

    MyUnion(MyUnion && other) { 
     if (other.have_value()) { 
     this->init_value(std::move(other.get_value())); 
     } else { 
     this->init_child(std::move(other.get_child())); 
     } 
    } 

    // Move assignment operator is easier, do that first. 
    // Note that if move ctors can throw, you can get a UB with this. 
    // So in most correct code, you would either ban such objects from 
    // appearing in your union, or try to make backup copies in order 
    // to recover from the exceptions. In this code, I will just 
    // assume that moving your object doesn't throw. 
    // In that case, it's just deinitialize self, then use code from 
    // move ctor. 

    MyUnion & operator = (MyUnion && other) { 
     this->deinitialize(); 
     if (other.have_value()) { 
     this->init_value(std::move(other.get_value())); 
     } else { 
     this->init_child(std::move(other.get_child())); 
     } 
     return *this; 
    } 

    // Copy ctor basically uses "copy and swap", but instead of 
    // swap, we use move assignment. This is exception safe, if 
    // move assignment is. 
    MyUnion & operator = (const MyUnion & other) { 
     MyUnion temp{other}; 
     *this = std::move(temp); 
     return *this; 
    } 

    // Dtor simply calls deinitialize. 
    ~MyUnion() { this->deinitialize(); } 

    private: 
    union { 
     ChildT* mChild; 
     ValueT mValue; 
    }; 
    bool mHaveValue; 

    // these next three methods are private helpers for you. 
    // the users of your class should not mess with these things, 
    // or UB is quite likely! 
    void deinitialize() { 
     if (mHaveValue) { 
     mValue.~ValueT(); 
     } else { 
     // pointer type has no dtor. But if you actually *own* the child, 
     // then you should call delete here I guess. 
     // Or, replace with `std::unique_ptr` and call 
     // that guys dtor. RAII is your friend, you can thank me later. 
     } 
    } 

    // Initialize the value, using perfect forwarding. 
    // Only do this if mValue is not currently initialized! 
    template <typename ... Args> 
    void init_value(Args && ... args) { 
     new (&mValue) ValueT(std::forward<Args>(args)...); 
     mHaveValue = true; 
    } 

    // Here, mChild is a raw pointer, so it doesn't make sense to 
    // make a similar initialization. But if you change it to be an RAII 
    // object, then you should probably do a similar pattern to above, 
    // with perfect forwarding. 
    void init_child(ChildT * c) { 
     mChild = c; 
     mHaveValue = false; 
    } 
}; 

注意:您通常不需要像这样滚动您自己的区分联合。很多时候,最好使用一些现有库,如boost::variant或评论中提到的expected类型之一。但是,让自己的小判别联合这样是

  • 并不难
  • 一个很好的锻炼
  • ,则有时需要出现在API边界或东西

一个好主意在很多情况下,使用联合是一种不必要的优化,只需一个struct即可。它需要更多的记忆才能表达对象,但这很少重要,并且可能会让团队更容易理解/更容易维护。

+0

我只显示了我想改变的代码的主要部分 - 没有内存泄漏之前,所以没有担心:)我的实现结果类似于你提供的,但我可能会从你偷了一些技巧!然而,感觉就像你错过了最重要的部分:目标是不需要用'new'分配内存。除此之外,我完全同意你的解决方案。 – pingul

+0

那么,我的代码不会在任何地方使用新的。如果你不希望'ChildT'是一个指针,那么在我的例子中使它成为一个类似ValueT的值类型。 –

+0

哦,我很困惑如何工作。 new(&mValue)ValueT(std :: forward (args)...);'做什么? – pingul

0

不,你不能memcpy东西哪些不是可以复制 - 而std::string当然不是。

此外,要访问此联合的非平凡成员,您必须首先调用一个位置new运算符 - 否则,它的构造函数将不会被调用,并且它将保持未初始化状态。

我基本上在工会中发现非平凡类型的用法通常是一种可疑的做法,但不是所有人都同意我的看法。