C , C++, C#

[C/C++] move

vhxpffltm 2019. 11. 14. 21:45

C/C++ 코드 분석을 하면서 필요한 부분들을 공부해보고자 한다.

 

원문은 https://modoocode.com/227 에 잘 정리되어 있다. 모든 내용의 출처는 이곳이다. 

원문내용을 보고 내용을 내 방식에 정리한 것이며, 원문의 출처에 자세하게 있다.

 

 

C언어를 하면서 학부과정에 swap에 대한 방법이 있다. 보통 이 과정에서 객체 메모리가 3번이나 '복사' 된다. 다음 코드와 코드를 실행한 결과이다. 

 

1
2
3
4
5
6
7
template <typename T>
void My_swap(T &a, T&b) {
    T tmp(a);//a는 lValue 라서 tmp의 복사 생성자가 호출, a가 차지하는 공간 만큼 메모리 할당 후,  a의 데이터가 복사됨
    //우리는 이것이 복사생성자가 아닌 이동생성자가 되길 원함. 하지만 a는 lValue라서 불가능
    a = b; // 2차적으로 복사가 발생, a가 차지하는 공만 메모리 할당후 b의 데이터 복사
    b = tmp; // 위와 같은 맥락
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

 

 

 

우리가 알고있는 call by value 에 따라 위 코드에서 객체도 복사된다. 주석을 참고하고 아래 그림에 있다.

 

 

위 그림과 같이 도식화 할 수 있다. 임의의 객체 tmp를 만들고 이 객체들의 값을 위와 같이 복사한다. 이렇게 하면 누가봐도 불필요한 메모리 복사가 이루어진다. 

 

이것을 str1은 987을 str2는 123을 가리키도록 포인터만 바꾸어주면 된다.

 

하지만 이것을 My_swap()으로만 바꾸기에는 문제가 있다.

 

1) 위 함수가 일반적인 타입 T 에 대해 작동해야 한다. 하지만 위 string_point 의 경우 MyString 에만 존재하는 필드이기 때문에 일반적인 타입 T 에 대해서는 작동하지 않는다.

 

-> 템플릿 특수화를 사용해서 해결한다.

    void My_swap(My &a, My &b) 로 해결

 

2) 하지만 string_point는 private 이기 때문에 My 클래스 내부에 선언해야한다. 이를 해결하기 위해 move를 사용한다. 

 

-> 우리가 원하는것은 T tmp(a) 가 이동생성자 가 되는것이다. 하지만 a가 lValue 이기 때문에 이 좌측값을 우측값으로 바꿔주는것이 필요하다.

 

move

move에 대한 예제를 잠깐 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
void My_swap(T &a, T&b) {
    T tmp(a);//a는 lValue 라서 tmp의 복사 생성자가 호출, a가 차지하는 공간 만큼 메모리 할당 후,  a의 데이터가 복사됨
    //우리는 이것이 복사생성자가 아닌 이동생성자가 되길 원함. 하지만 a는 lValue라서 불가능
    a = b; // 2차적으로 복사가 발생, a가 차지하는 공만 메모리 할당후 b의 데이터 복사
    b = tmp; // 위와 같은 맥락
}
 
std::cout << "Example...................... " << std::endl;
    My str3("abc");
    std::cout << "Previous......" << std::endl;
    std::cout << "str3 : ";
    str3.println();
    std::cout << "after........." << std::endl;
    My str4(std::move(str3));//move는 lvalue를 이동 가능한 값으로 캐스팅해준다. 이동 된후 처음에 쓰인거에 다시 접근하면 안됨!! 
    std::cout << "str4 : ";
    str4.println();
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

 

이전 이동생성자 코드에 위 코드만 추가한 것이다. 

 

결과가 이동 생성자 호출이 출력됨을 보이고 있다. 바로 My str4(std::move(str3)) 에 의해 이같은 결과를 얻은것이다. move는 우측값으로 캐스팅하는 역할을 한다고 생각하면 된다.

 

다만 move의 중요한점은 이 과정을 수행한 후 move에 사용한 객체를 건드리면 안된다는 것을 명심하자.

마지막줄에 str3.println() 을 하면 프로그램이 제대로 실행되지 않을것이다.

 

이제 전체코드와 함께 위의 My_swap을 변경해보자.

 

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include <iostream>
#include "My.h"
 
My::My() {
    std::cout << "생성자 호출!!" << std::endl;
    string_point = nullptr;
    s_len = 0;
    memory = 0;
}
 
My::My(const char *str) {
    std::cout << "인자있는 생성자 호출!!" << std::endl;
    s_len = strlen(str);
    memory = s_len;
    string_point = new char[s_len];
    for (int i = 0; i != s_len; i++) string_point[i] = str[i];
}
 
My::My(const My &str) {
    std::cout << "(객체를 인자로) 복사생성자 호출 " << std::endl;
    s_len = str.s_len;
    string_point = new char[s_len];
    for (int i = 0; i != s_len; i++) string_point[i] = str.string_point[i];
}
My::My(My &&str) noexcept {
    std::cout << "&&로 이동 생성자 호출" << std::endl;
    s_len = str.s_len;
    memory = str.memory;
    string_point = str.string_point;
 
    str.string_point = nullptr;
}
My::~My() {
    delete[] string_point;
}
void My::reserve(int sz) {
    if (sz > memory) {
        char *prev_string_point = string_point;
        string_point = new char[sz];
        memory = sz;
        for (int i = 0; i != s_len; i++) string_point[i] = prev_string_point[i];
        if (prev_string_point != nullptr) delete[] prev_string_point;
    }
}
 
My My::operator+(const My&s) {
    std::cout << "operator+ " << std::endl;
    My str; //빈 My객체인 str생성, (생성자 호출!) 출력이 될거임, 이후 reserve로 공간을 할당
    str.reserve(s_len + s.s_len);
    for (int i = 0; i < s_len; i++) str.string_point[i] = string_point[i];
    for (int i = 0; i < s.s_len; i++) str.string_point[s_len + i] = s.string_point[i];
    str.s_len = s_len + s.s_len;
    return str;
}
 
My &My::operator=(const My &s) {
    std::cout << "복사!" << std::endl;
    if (s.s_len > memory) {
        delete[] string_point;
        string_point = new char[s.s_len];
        memory = s.s_len;
    }
    s_len = s.s_len;
    for (int i = 0; i != s_len; i++) {
        string_point[i] = s.string_point[i];
    }
    return *this;
}
My &My::operator=(My && s) {
    std::cout << "이동!!" << std::endl;
    string_point = s.string_point;
    memory = s.memory;
    s_len = s.s_len;
 
    s.string_point = nullptr;
    s.memory = 0;
    s.s_len = 0;
 
    return *this;
}//rValue 이동을 위한 새로운 메서드, 값만 복사해주면 된다.
 
int My::length() const { return s_len; }
 
void My::print() {
    for (int i = 0; i != s_len; i++std::cout << string_point[i];
}
void My::println() {
    for (int i = 0; i != s_len; i++std::cout << string_point[i];
    std::cout << std::endl;
}
 
template <typename T>
void My_swap(T &a, T&b) {
    T tmp(a);//a는 lValue 라서 tmp의 복사 생성자가 호출, a가 차지하는 공간 만큼 메모리 할당 후,  a의 데이터가 복사됨
    //우리는 이것이 복사생성자가 아닌 이동생성자가 되길 원함. 하지만 a는 lValue라서 불가능
    a = b; // 2차적으로 복사가 발생, a가 차지하는 공만 메모리 할당후 b의 데이터 복사
    b = tmp; // 위와 같은 맥락
}
//이 swap함수는 객체의 주소값, 보통 래퍼런스에 의한 복사, 포인터만 바꾸어서 복사를 하면 되는것을 알고있다.
//예제에서는 4바이트 단순 int라 문제가 아니지만 우리가 이것을 하는이유는
//커다란 시스템 내부를 다룰때 아주 중요하기 때문이다.
//문제는 string_point private이기 때문에 My 클래스 내부에 관련함수를 만들어야 한다.
// 굳이 이것을 재정의할 필요가 없음... 이를 좀 더 깔끔하게 해결해보자.
//lValue인 a를 rValue로 바꿔야 이동 생성자로 할 수 있다. 그것이 move이다.
template <typename T>
void My_swap2(T &a, T &b) {
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}
//move로 해결한 swap 함수
//a = ... 와 b = ... 때문에 operator= 가 복사를 수행함
//operator= 을 &&로 이동 생성자를 호출하도록 하나 생성해줘야 복사가 아닌 이동이 
//이루어지고 프로그렘이 정상적으로 실행된다.(My &operator=(My && s)) 메서드 확인
 
int main() {
 
    My str5("1234");
    My str6("9876");
    std::cout << "swap 전-=-=-=-=-=-=-=-=-=-=-=" << std::endl;
    std::cout << "str 5 : ";
    str5.println();
    std::cout << "str 6 : ";
    str6.println();
    std::cout << "swap 후-=-=-=-=-=-=-=-=-=-=-=" << std::endl;
    My_swap2(str5, str6);
    std::cout << "str 5 : ";
    str5.println();
    std::cout << "str 6 : ";
    str6.println();
 
 
 
}
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter
 

 

이전에 사용한 함수는 My_swap() 이고 move를 사용한 함수는 My_swap2()이다.

 

위 코드에서는 정답을 다 해놨지만 'My &opeerator=' 를 알아보고 가야된다. 우선 이게 없으면 이동 생성자를 한번 호출하고 나머지는 복사가 일어난다.

 

이를 위해 위의 우측값에만 특이하게 오버로딩 되는 이 함수를 정의해주었다. string_point의 값만을 복사해주는 역할이다.그리고 사용한 &&s 는 우측값이니 s의 문자열은 사라지더라도 신경쓰지 않아도 된다.

 

아래는 출력 결과이다.

 

결국 move는 좌측값을 이동 가능한 값으로 캐스팅하는것. 컴파일러가 move 함수가 리턴하는 값을 이동 가능 하구나 라고 생각하게 해준다.

 

'C , C++, C#' 카테고리의 다른 글

[C/C++] #if #elif #endif 조건부 컴파일  (0) 2019.12.04
[C/C++] #define 매크로, #undef, size_t  (0) 2019.12.03
[C/C++] 이동 생성자  (0) 2019.11.03
[C++] 가상함수 테이블  (0) 2019.03.12
C++ 가상함수 이해정리  (0) 2019.03.06