使用unique_ptr的联合体

20

尝试在联合体(union)中使用unique_ptr时,当我尝试std::move或std::make_unique时,会导致段错误。

#include <iostream>
#include <memory>

union myUnion{
    struct{std::unique_ptr<float> upFloat;}structUpFloat;
    struct{std::unique_ptr<int> upInt;}structUpInt;
    myUnion(){}
    ~myUnion(){}
};
struct myStruct{
    int x;
    myUnion num;

};
int main()
{
    myStruct aStruct, bStruct;
    aStruct.x = 1;
    bStruct.x = 2;

    auto upF = std::make_unique<float>(3.14);
    auto upI = std::make_unique<int>(3);

    aStruct.num.structUpFloat.upFloat = std::move(upF);
    bStruct.num.structUpInt.upInt = std::move(upI);

    std::cout << "aStruct float = " << *aStruct.num.structUpFloat.upFloat << std::endl;
    std::cout << "bStruct int = " << *bStruct.num.structUpInt.upInt << std::endl;
    return 0;
}

然而,使用普通指针可以正常工作:

#include <iostream>
#include <memory>

union myUnion{
    struct{float *pFloat;}structPFloat;
    struct{int *pInt;}structPInt;
    myUnion(){}
    ~myUnion(){}
};
struct myStruct{
    int x;
    myUnion num;

};
int main()
{
    myStruct aStruct, bStruct;
    aStruct.x = 1;
    bStruct.x = 2;

    auto upF = std::make_unique<float>(3.14);
    auto upI = std::make_unique<int>(3);

    aStruct.num.structPFloat.pFloat = upF.get();
    bStruct.num.structPInt.pInt = upI.get();

    std::cout << "aStruct float = " << *aStruct.num.structPFloat.pFloat << std::endl;
    std::cout << "bStruct int = " << *bStruct.num.structPInt.pInt << std::endl;
    return 0;
}

我使用的是clang.3.4.2或gcc.4.9.0。所以我想我在这里做错了什么。感谢任何帮助。

编辑:

好的,分享我最终确定的代码可能是一个好习惯。非常感谢所有指导我使用放置new来管理变体成员指针生命周期的人。

#include <memory>
#include <iostream>
#include <vector>
struct myStruct
{
public:
    union
    {
        std::unique_ptr<float> upFloat;
        std::unique_ptr<int> upInt;
    };
    enum class unionType {f, i,none} type = unionType::none; // Keep it sane
    myStruct(){}
    myStruct(std::unique_ptr<float> p)
    {
        new (&upFloat) std::unique_ptr<float>{std::move(p)};
        type = unionType::f;
    }
    myStruct(std::unique_ptr<int> p)
    {
        new (&upInt) std::unique_ptr<int>{std::move(p)};
        type = unionType::i;
    }
    ~myStruct()
    {
        switch (type)
        {
            case unionType::f: upFloat.~unique_ptr<float>(); break;
            case unionType::i: upInt.~unique_ptr<int>(); break;
        }
    }
};

int main()
{
    std::vector<std::unique_ptr<myStruct>> structVec;
    structVec.push_back(std::make_unique<myStruct>(std::make_unique<float>(3.14f)));
    structVec.push_back(std::make_unique<myStruct>(std::make_unique<int>(739)));
    structVec.push_back(std::make_unique<myStruct>());
    structVec.push_back(std::make_unique<myStruct>(std::make_unique<float>(8.95f)));
    structVec.push_back(std::make_unique<myStruct>(std::make_unique<int>(3)));
    structVec.push_back(std::make_unique<myStruct>());

    for(auto &a: structVec)
    {
        if(a->type == myStruct::unionType::none)
        {
            std::cout << "Struct Has Unallocated Union" << std::endl;
        }
        else if(a->type == myStruct::unionType::f)
        {
            std::cout << "Struct float = " << *a->upFloat << std::endl;
        }
        else
        {
            std::cout << "Struct int = " << *a->upInt << std::endl;
        }
        std::cout << std::endl;
    }

    return 0;
}

输出:

结构体浮点数 = 3.14

结构体整数 = 739

带有未分配联合体的结构体

结构体浮点数 = 8.95

结构体整数 = 3

带有未分配联合体的结构体


1
如果两个指针指向同一个内存单元,它们仍然可以是唯一的吗? - Tuğrul
2
@πάνταῥεῖ 无益/无建设性或不适用。 - jacksawild
@Tuğrul 我认为只有其中一个存在,或者至少最大的那个存在空间。我错过了什么吗? - jacksawild
1
@πάνταῥεῖ:不,调试器在这种高度破碎的情况下是无法帮助的。调试器无法跟踪联合体的活动成员。(实际上,在这样的程序上使用调试器可能会导致未定义的行为,因为它将从非活动的联合成员中读取) - Ben Voigt
@BenVoigt 在阅读了您的回答后,您可能是正确的关于这一点。 - πάντα ῥεῖ
显示剩余3条评论
4个回答

16
改变联合体的活动成员需要特殊注意对象生命周期。C++标准(9.5p4)指出:

注意: 通常情况下, 改变联合体的活动成员需使用显式的析构函数调用和placement new运算符。

当成员为普通数据时, 它通常能够"正常工作", 即使你没有调用构造函数(使用placement new), 也没有调用析构函数。这是因为具有平凡初始化的对象的生命周期在获得足够大小和正确对齐的存储空间时开始, 而联合体提供了这种存储空间。
现在你有了具有非平凡构造函数和析构函数的成员。它们的生命周期并不是在获得存储空间时就开始的, 你还必须使初始化完成,这意味着需要使用placement new。跳过析构函数调用也不安全, 如果那些析构函数会产生程序依赖的副作用, 那么你将会得到未定义的行为(例如unique_ptr的析构函数会释放它所管理的目标内存)。
因此, 在成员的生命周期尚未开始时调用移动赋值运算符是未定义行为。

1
有道理,谢谢。如果我能接受两个答案的话,我会这么做的。 - jacksawild

13

对于无限制的联合,你需要自己管理一些结构体和析构函数。

以下内容可能有所帮助:

union myUnion{
    std::unique_ptr<float> upFloat;
    std::unique_ptr<int> upInt;

    myUnion(){ new (&upFloat) std::unique_ptr<float>{};}
    ~myUnion() {}
};

class myStruct
{
public:
    ~myStruct()
    {
        destroy();
    }

    void destroy()
    {
        switch (type)
        {
            case unionType::f: num.upFloat.~unique_ptr<float>(); break;
            case unionType::i: num.upInt.~unique_ptr<int>(); break;
        }
    }

    void set(std::unique_ptr<int> p)
    {
        destroy();
        new (&num.upInt) std::unique_ptr<int>{std::move(p)};
        type = unionType::i;
    }
    void set(std::unique_ptr<float> p)
    {
        destroy();
        new (&num.upFloat) std::unique_ptr<float>{std::move(p)};
        type = unionType::f;
    }

public:
    enum class unionType {f, i} type = unionType::f; // match the default constructor of enum
    myUnion num;
};

int main()
{
    myStruct aStruct, bStruct;

    aStruct.set(std::make_unique<float>(3.14f));
    bStruct.set(std::make_unique<int>(3));

    std::cout << "aStruct float = " << *aStruct.num.upFloat << std::endl;
    std::cout << "bStruct int = " << *bStruct.num.upInt << std::endl;
    return 0;
}

在C++17中,你可以使用std::variant代替自己的结构体。


2
如果您使用匿名联合体,使整个类成为变体(类似于联合体),那么这将更加健壮。现在可以在没有提供所有所需逻辑的包装器的情况下使用联合体。 - Ben Voigt

4
这个参考文献得知:
如果一个联合(union)包含一个具有非平凡特殊成员函数(复制/移动构造函数,复制/移动赋值运算符或析构函数)的非静态数据成员,则该函数默认在联合中被删除,并需要程序员显式地定义。
我猜您将指针包装在简单结构中的原因是由于上述段落所施加的限制而无法以其他方式构建。
您所做的是绕过编译器的安全保护措施,很可能导致代码出现未定义行为

将它们包装在结构体中是来自更复杂版本的,而我在这里简化了它们。去掉这些包装仍然会产生可编译的代码。这里没有绕过任何东西。你对删除的构造函数是正确的,通过定义自己的默认构造函数/析构函数,我正在绕过一个错误。我认为解决方案在缺失的复制/赋值/移动构造函数声明中,但我一直无法弄清楚。 - jacksawild
@jacksawild:你的问题与特殊成员关系不大——你没有在任何地方分配整个联合体,这与变量成员的生命周期有关。 - Ben Voigt

3

根据标准§12.6.2[class.base.init]/p8(强调添加):

在非委托构造函数中,如果给定的非静态数据成员或基类未被赋值为一个“mem-initializer-id”(包括没有“mem-initializer-list”的情况,因为构造函数没有“ctor-initializer”,并且该实体不是抽象类的虚基类(10.4),则

  • 如果该实体是具有“brace-or-equal-initializer”的非静态数据成员,则按照8.5的规定初始化该实体;
  • 否则,如果该实体是变量成员(9.5),则不执行任何初始化
  • [...]

联合成员是变量成员,这意味着unique_ptr未被初始化。特别地,没有任何构造函数,甚至没有默认构造函数被调用。从技术上讲,这些unique_ptr的生命周期甚至都没有开始。

unique_ptr移动赋值运算符必须删除unique_ptr当前持有的内容,但您正在将其移动分配到一个未初始化的包含垃圾值的“unique_ptr”。结果,您的移动赋值很可能导致尝试删除垃圾指针,从而导致段错误。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接