0%

c++ union 中使用非POD对象

c++ union 中使用非POD对象

c++ 定义

1
2
3
4
5
6
7
8
9
(C++11 前)
联合体不能含有带非平凡特殊成员函数(复制构造函数、复制赋值运算符或析构函数)的非静态数据成员。

(C++11 起)
若联合体含有带非平凡特殊成员函数(复制/移动构造函数,复制/移动赋值,或析构函数)的非静态数据成员,则联合体中的该函数默认被弃置,且需要程序员显式定义它。

若联合体含有带非平凡默认构造函数的非静态数据成员,则该联合体的默认构造函数默认被弃置,除非联合体的变体成员拥有一个默认成员初始化器。

至多一个变体成员可以拥有默认成员初始化器。
  1. c++11 起, 可以使用 非POD对象, 但是 c++不会帮你调用默认的构造和析构函数, 必须自己定义

对于POD数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
union S {
std::int32_t n; // 占用4字节
std::uint16_t s[2]; // 占用4字节
std::uint8_t c; // 占用1字节
}; // 整体占用4字节

TEST(build_in, 1) {
S s = {0x12345678}; // 初始化第一个成员,当前s.n为活跃成员
// 于此点,读取 s.s 或 s.c 是未定义行为
std::cout << std::hex << "s.n = " << s.n << '\n';
s.s[0] = 0x0011; // s.s 现在是活跃成员
// 在此点,读取 s.n 或 s.c 是未定义行为
std::cout << "s.c is now " << +s.c << '\n' // 11 or 00, 依赖平台实现
<< "s.n is now " << s.n << '\n'; // 12340011 or 00115678
}
  1. 这个很好理解, c++只是提供了一块内存, 保证可以放下最大的, 怎么解析是程序员的事情

对于非POD对象

用于测试的c++ class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

namespace MyT {

class MyClass {
public:
std::string name{};
std::string ToString() const {
return "MyClass:" + name + "\t@" + std::to_string((intptr_t)this);
}
MyClass(){LOG_DEBUG(ToString())};
MyClass(const std::string& _name) {
name = _name;
LOG_DEBUG(ToString())
}
MyClass(const MyClass& obj) {
name = obj.name;
LOG_DEBUG("this: " << ToString() << "\tobj: " << obj.ToString())
};
MyClass(MyClass&& obj) {
LOG_DEBUG("\tobj: " << obj.ToString())
std::swap(name, obj.name);
LOG_DEBUG("this: " << ToString())
};
virtual ~MyClass(){LOG_DEBUG(ToString())};
MyClass& operator=(const MyClass& obj) {
LOG_DEBUG("this: " << ToString() << "\tobj: " << obj.ToString())
name = obj.name;
LOG_DEBUG("this: " << ToString() << "\tobj: " << obj.ToString())
return *this;
};

MyClass& operator=(MyClass&& obj) {
LOG_DEBUG("\tobj: " << obj.ToString())
std::swap(name, obj.name);
LOG_DEBUG("this: " << ToString())
return *this;
};
};
} // namespace MyT
  1. 调用构造, 拷贝, 移动, 析构时都打条log, 方便我们定位
  2. 测试代码:
    1. 需要注意: MyClass c4 = c2 会调用复制构造函数, MyClass c5; c5 = c2; 才会调用operator=(&)
1
2
3
4
5
6
7
8
9
10
11
12
13
TEST(my_class, 1) {
using MyT::MyClass;
MyClass c1{};
MyClass c2{"khalid"};
MyClass c3(c2);
// 没有调用 operator= 而是使用了复制构造函数, 这是为了做优化
// 而赋值操作符要求‘=’的左右对象均已存在,它的作用就是把‘=’右边的对象的值赋给左边的对象
MyClass c4 = c2;
// 这种 的左右对象均已存在 才会
MyClass c5;
c5 = c2;
MyClass c6(std::move(c2));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[1]    [debug]    [MyClass:	@140733882434096]   [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:44    MyT::MyClass::MyClass()]     [2021-05-25 11:04:36.885673]    [0x00005efc]    [0x00007f3d8c6ad780]    []
[2] [debug] [MyClass:khalid @140733882434080] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:47 MyT::MyClass::MyClass(const string&)] [2021-05-25 11:04:36.885830] [0x00005efc] [0x00007f3d8c6ad780] []
[3] [debug] [this: MyClass:khalid @140733882434064 obj: MyClass:khalid @140733882434080] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:51 MyT::MyClass::MyClass(const MyT::MyClass&)] [2021-05-25 11:04:36.885914] [0x00005efc] [0x00007f3d8c6ad780] []
[4] [debug] [this: MyClass:khalid @140733882434048 obj: MyClass:khalid @140733882434080] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:51 MyT::MyClass::MyClass(const MyT::MyClass&)] [2021-05-25 11:04:36.885991] [0x00005efc] [0x00007f3d8c6ad780] []
[5] [debug] [MyClass: @140733882434032] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:44 MyT::MyClass::MyClass()] [2021-05-25 11:04:36.886049] [0x00005efc] [0x00007f3d8c6ad780] []
[6] [debug] [this: MyClass: @140733882434032 obj: MyClass:khalid @140733882434080] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:60 MyT::MyClass& MyT::MyClass::operator=(const MyT::MyClass&)] [2021-05-25 11:04:36.886101] [0x00005efc] [0x00007f3d8c6ad780] []
[7] [debug] [this: MyClass:khalid @140733882434032 obj: MyClass:khalid @140733882434080] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:62 MyT::MyClass& MyT::MyClass::operator=(const MyT::MyClass&)] [2021-05-25 11:04:36.886158] [0x00005efc] [0x00007f3d8c6ad780] []
[8] [debug] [ obj: MyClass:khalid @140733882434080] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:54 MyT::MyClass::MyClass(MyT::MyClass&&)] [2021-05-25 11:04:36.886211] [0x00005efc] [0x00007f3d8c6ad780] []
[9] [debug] [this: MyClass:khalid @140733882434016] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:56 MyT::MyClass::MyClass(MyT::MyClass&&)] [2021-05-25 11:04:36.886264] [0x00005efc] [0x00007f3d8c6ad780] []
[10] [debug] [MyClass:khalid @140733882434016] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:04:36.886315] [0x00005efc] [0x00007f3d8c6ad780] []
[11] [debug] [MyClass:khalid @140733882434032] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:04:36.886365] [0x00005efc] [0x00007f3d8c6ad780] []
[12] [debug] [MyClass:khalid @140733882434048] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:04:36.886416] [0x00005efc] [0x00007f3d8c6ad780] []
[13] [debug] [MyClass:khalid @140733882434064] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:04:36.886466] [0x00005efc] [0x00007f3d8c6ad780] []
[14] [debug] [MyClass: @140733882434080] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:04:36.886515] [0x00005efc] [0x00007f3d8c6ad780] []
[15] [debug] [MyClass: @140733882434096] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:04:36.886564] [0x00005efc] [0x00007f3d8c6ad780] []
Process finished with exit code 0

包含非POD的union

一个错误的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// error
namespace t0 {
union A {
int a;
double b;
std::string c;
MyClass d;
std::shared_ptr<MyClass> e;
};
// default constructor of 'A0' is implicitly deleted because variant field 'c'
// has a non-trivial default constructor
// c 不是平凡类型, 必须自定义构造函数
TEST(adt,1)
{
A s;
}
}
  1. 无法通过编译, c 不是POD对象, 必须自定义构造函数

一个可用的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 主动调用析构函数, 包装下免得调错
template <class T>
inline void DestoryAt(T& t) {
t.~T();
}
namespace t1 {
union A {
std::string c;
MyT::MyClass d;
std::shared_ptr<MyT::MyClass> e;
A() {} // 因为std::string拥有是非平凡的的数据类型,
~A() {} // 则A必须自定义构造函数和析构函数,否则无法进行实例化
// 如果想实现复制语义,还得自定义复制(构造)函数
};
TEST(adt, 1) {
A s;
// 激活 d
new (&s.d) MyT::MyClass();
// d可以使用了
s.d.name = "in union";
LOG_DEBUG("&s.d " << &s.d)
// 必须手动释放 d
// union 不会自动调用析构函数
// 下面两种方法都行, 包装下免得调错
DestoryAt(s.d);
// s.d.~MyClass();

// 激活e
new (&s.e) std::shared_ptr<MyT::MyClass>();
// 初始化一个 MyClass
s.e = std::make_shared<MyT::MyClass>();
s.e->name = "union ptr";
// 释放e
DestoryAt(s.e);
}
} // namespace t1
1
2
3
4
5
[1]    [debug]    [MyClass:	@140730698243312]   [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:44    MyT::MyClass::MyClass()]     [2021-05-25 11:13:42.707161]    [0x0000648e]    [0x00007f5df07a5780]    []
[2] [debug] [&s.d 0x7ffe6b471cf0] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:126 virtual void t1::adt_1_Test::TestBody()] [2021-05-25 11:13:42.707324] [0x0000648e] [0x00007f5df07a5780] []
[3] [debug] [MyClass:in union @140730698243312] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:13:42.707407] [0x0000648e] [0x00007f5df07a5780] []
[4] [debug] [MyClass: @22852584] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:44 MyT::MyClass::MyClass()] [2021-05-25 11:13:42.707480] [0x0000648e] [0x00007f5df07a5780] []
[5] [debug] [MyClass:union ptr @22852584] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:13:42.707535] [0x0000648e] [0x00007f5df07a5780] []
  1. 这里构造和析构函数啥都没干, 也就是说不会自动构造和析构
  2. 我们必须手动构造(使用 placement new), 手动析构(显示调用~ClassName())
  3. 但这是证明union中放非POD对象是可行的

一种常见的封装方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

namespace t2 {
// 使用struct包装
struct SA {
enum class Tag : int {
STRING = 0,
MY_CLASS,
MY_CLASS_SPTR,
};
const Tag tag;
union {
std::string c;
MyT::MyClass d;
std::shared_ptr<MyT::MyClass> e;
};
SA() = delete;
// TODO
SA(const SA&) = delete;
SA(const SA&&) = delete;
SA& operator=(const SA&) = delete;
SA& operator=(SA&&) = delete;


SA(Tag t) : tag(t) {
switch (tag) {
case Tag::STRING:
new (&c) decltype(c);
break;
case Tag::MY_CLASS:
new (&d) decltype(d);
break;
case Tag::MY_CLASS_SPTR:
new (&e) decltype(e);
break;
default:
assert(false);
break;
}
}

virtual ~SA() {
switch (tag) {
case Tag::STRING:
DestoryAt(c);
break;
case Tag::MY_CLASS:
DestoryAt(d);
break;
case Tag::MY_CLASS_SPTR:
DestoryAt(e);
break;
default:
assert(false);
break;
}
}
};
TEST(adt, struct_type) {
{
SA myclass(SA::Tag::MY_CLASS);
myclass.d.name = "SA";
LOG_DEBUG(myclass.d.ToString());
}

{
SA myclassPtr(SA::Tag::MY_CLASS_SPTR);
myclassPtr.e = std::make_shared<decltype(myclassPtr.e)::element_type>();
myclassPtr.e->name = "SAP";
LOG_DEBUG(myclassPtr.e->ToString());
}

}
} // namespace t2
1
2
3
4
5
6
[1]    [debug]    [MyClass:	@140731968851312]   [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:44    MyT::MyClass::MyClass()]     [2021-05-25 11:15:15.048335]    [0x0000659b]    [0x00007fec328fe780]    []
[2] [debug] [MyClass:SA @140731968851312] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:203 virtual void t2::adt_struct_type_Test::TestBody()] [2021-05-25 11:15:15.048452] [0x0000659b] [0x00007fec328fe780] []
[3] [debug] [MyClass:SA @140731968851312] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:15:15.048486] [0x0000659b] [0x00007fec328fe780] []
[4] [debug] [MyClass: @26641416] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:44 MyT::MyClass::MyClass()] [2021-05-25 11:15:15.048515] [0x0000659b] [0x00007fec328fe780] []
[5] [debug] [MyClass:SAP @26641416] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:210 virtual void t2::adt_struct_type_Test::TestBody()] [2021-05-25 11:15:15.048543] [0x0000659b] [0x00007fec328fe780] []
[6] [debug] [MyClass:SAP @26641416] [/tmp/tmp.s62aUyvWJo/src/cpp_language/union_adt_test/main.cpp:58 virtual MyT::MyClass::~MyClass()] [2021-05-25 11:15:15.048571] [0x0000659b] [0x00007fec328fe780] []
  1. 这里使用 TLV的思路, 使用Tag+Value进行封装, 使用struct进行包装
  2. 这样外部使用的时候就类似在使用一个struct而非union, 实现自动构造&析构
  3. 当然, 这只是个简单的例子, 实际使用的时候, 还可以实现各种构造函数和operator, 使他更像struct, 还可以把union对象private化, 使用函数获取, 这样就可以校验类型是否对的上
    1. 当然, 一般都是用脚本生成的, 我在项目中是用Python写了个protobuf 的protoc后端, 用jinja2直接渲染出代码用