2 분 소요

개요

기존의 공용체에서는 생성자/소멸자/가상 함수가 없는 Trivial 타입공용체의 멤버가 될 수 있었는데요(공용체 참고),

C++11 부터는 이를 완화하였습니다. 대신 멤버들의 생성자소멸자가 호출되지 않으므로, 이를 수동으로 제어해야 합니다.

다음에서 MyUnion_11은 non-Trivial 타입인 AB, Derived를 멤버로 사용하고, 크기가 가장 큰 개체의 크기만큼 메모리를 할당합니다.

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
// 생성자와 소멸자가 있어서 non-Trivial 타입입니다.
class A {
    int m_X;
    int m_Y;
public:
    A(int x, int y) : m_X{x}, m_Y{y} {
        std::cout << "A : Constructor" << std::endl;
    }
    ~A() {
        std::cout << "A : Destructor" << std::endl;    
    }
    int GetX() const {return m_X;}
    int GetY() const {return m_Y;}

    void SetX(int x) {m_X = x;}
    void SetY(int y) {m_Y = y;}
};

// 생성자와 소멸자가 있어서 non-Trivial 타입입니다.
class B {
    std::string m_Str;
public:
    
    explicit B(const char* str) : m_Str{str} {
        std::cout << "B : Constructor" << std::endl;    
    }
    ~B() {
        std::cout << "B : Destructor" << std::endl;       
    }
    const std::string& GetString() const {return m_Str;}
};

class Base {
public:
    virtual int Func() {return 1;} // #1
};

// 생성자와 소멸자, 가상 함수가 있어서 non-Trivial 타입입니다.
class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived : Constructor" << std::endl;    
    }
    ~Derived() {
        std::cout << "Derived : Destructor" << std::endl;  
    }
    
    virtual int Func() override {return 2;} // #2        
};

union MyUnion_11 {
    A m_A; // non-Trivial 타입입니다.
    B m_B; // non-Trivial 타입입니다.
    Derived m_Derived; // non-Trivial 타입입니다.
    MyUnion_11() {
        std::cout << "MyUnion_11 : Constructor" << std::endl;  
    }
    ~MyUnion_11() {
        std::cout << "MyUnion_11 : Destructor" << std::endl;  
    }
};

// A, B, Derived 는 non-trival 입니다.
EXPECT_TRUE(std::is_trivial<A>::value == false);
EXPECT_TRUE(std::is_trivial<B>::value == false);  
EXPECT_TRUE(std::is_trivial<Derived>::value == false); 

// MyUnion 은 A, B, Derived 중 크기가 큰 개체만큼 메모리를 할당합니다.
EXPECT_TRUE(sizeof(Derived) <= sizeof(A) && sizeof(A) <= sizeof(B) && sizeof(B) == sizeof(MyUnion_11));

// A, B, Derived의 생성자와 소멸자가 호출되지 않습니다.
MyUnion_11 obj;

상기 코드를 실행하면, A, B, Derived생성자소멸자는 호출되지 않고, MyUnion_11생성자소멸자가 호출됩니다.

1
2
MyUnion_11 : Constructor
MyUnion_11 : Destructor

이러면 멤버 개체 소멸이 되지 않아 문제죠.

따라서, MyUnion_11을 사용하기 위해선 다음처럼 각 멤버의

  1. 위치 지정 생성(Placement New)를 이용하여 생성자를 호출하고,
  2. 소멸자를 호출해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MyUnion_11 obj;

// A를 사용하기 위해 A의 생성자를 호출합니다.
new (&obj.m_A) A{10, 20};
EXPECT_TRUE(obj.m_A.GetX() == 10 && obj.m_A.GetY());

// A말고 B를 사용하기 위해 기존 A는 소멸시키고 B의 생성자를 호출합니다.
obj.m_A.~A();
new (&obj.m_B) B{"Hello"};
EXPECT_TRUE(obj.m_B.GetString() == "Hello");

// B 말고 Derived를 사용하기 위해 기존 B는 소멸시키고 Derived의 생성자를 호출합니다.
obj.m_B.~B();
new (&obj.m_Derived) Derived;
EXPECT_TRUE(obj.m_Derived.Func() == 2);

// obj 사용이 끝나서 Derived 도 소멸합니다.
obj.m_Derived.~Derived();

실행 결과를 보면 각 멤버의 생성자소멸자가 호출된 것을 확인 할 수 있습니다.

1
2
3
4
5
6
7
8
MyUnion_11 : Constructor
A : Constructor
A : Destructor
B : Constructor
B : Destructor
Derived : Constructor
Derived : Destructor
MyUnion_11 : Destructor

수동으로 생성자소멸자를 제어해야 해서 많이 불편하지만, 서로 다른 개체 타입을 관리해야 하고, 상황에 따라 여러개 중에 1개만 활성화해서 관리해야 할때 메모리를 알뜰하게 사용할 수 있습니다.

(C++17~) variant가 추가되어 타입이 다른 여러 데이터들을 동일한 메모리 공간에서 쉽게 관리할 수 있습니다.

태그:

카테고리:

업데이트:

댓글남기기