-
[30] - (C++) For each, Move Semantics, Lambdas, Uniform Initialization - 2Graphics 2021. 8. 3. 21:24
아직 Move semantics에 대한 이야기가 끝난 것이 아닙니다. 이 move constructor가 언제 호출되는지 알아야 실험 코드를 작성하든 말든 할 것입니다. 그런데 어떤 상황에서는 move ctor가 호출이 되어야 하는데 호출이 안 되는 경우가 있었습니다. 반대로 안 되어야 하는데 되는 경우도 있었습니다. 그래서 애를 많이 먹었는데 이유를 찾아보니 C++17 표준부터 규칙이 약간 바뀌었다는 점과 컴파일러에 달렸다는 사실을 알게 되었습니다. 그리고 오류가 나지 않은 것은 단순히 운이 좋았던 것 뿐이라는 사실도 알았습니다. 기본적으로 다음의 이유들이 move ctor을 invoke합니다.
1. 어떤 object가 std::move로 생성될 때.
2. 어떤 object가 std::forward<T>(sth)으로 생성되고 T는 lvalue reference가 아닐 때.
3. 어떤 object의 initialiser가 일시적인 경우.
4. .. 등등등등
다음 코드는 어떤 예제의 코드를 그대로 갖고 온 것입니다.(https://www.cprogramming.comc++11/rvalue-references-and-move-semantics-in-c++11.html)
이게 안 되어야 정상인데 운이 좋으면 아무런 문제 없이 잘됩니다. 안 된다는 가정하에 설명을 읽어봤습니다. 구조를 조금 이해를 하면 됩니다. ArrayWrapper의 Move Constructor가 호출되는 것은 자명합니다. 그리고 제 생각엔 move ctor가 호출되면서 metadata의 move ctor이 호출될 것 같았습니다. 그게 아니었습니다.

Meta Data와 ArrayWrapper의 move ctor와 copy ctor에 한 줄의 코드를 각각 삽입하고 돌려보면 다음과 같은 결과를 얻습니다.

복사 생성자가 호출되고 있는 모습입니다 이유는 move ctor의 parameter인 other에 있습니다. 분명 배우기로는 other는 rvalue입니다. 그런데 알고 보니 rvalue가 아니랍니다. 듣고 보니 고개가 끄덕여집니다. rvalue reference는 rvalue가 아닙니다. rvalue는 rvalue가 맞지만 rvalue reference는 rvalue가 아니고 lvalue입니다. 따라서 _name(other._name)이라든가 _size(other._size)은 move ctor가 아니라 copy ctor를 호출했던 것입니다.
위의 코드를 디버그 하며 Callstack을 유심히 살펴보면 왜 그랬는지 이유를 알 수 있습니다. 처음 ArrayWrapper 인스턴스를 만들 때를 제외하고 함수 GetArraystack은 인스턴스 B를 parameter로 받으며 ArrayWrapper의 복사 생성자를 한 번 호출합니다.

ArrayWrapper의 copy ctor 복사를 위해서 metadata까지 복사를 시도합니다. 한꺼풀씩 내려갑니다. 바로 밑 코드를 실행하면 copy ctor를 호출하는 것이 당연해 보입니다. 현재는 함수의 call by value에 의한 복사가 진행 중이기 때문입니다. 실제로 확인해보면 Metadata의 copy ctor가 실행됩니다.


진짜 문제는 GetArrayWrapper가 이 object에 대한 rvalue를 반환하면서 나타납니다.


Callstack을 보면 이제 GetArrayWrapper의 return을 위해서 ArrayWrapper의 rvalue constructor가 호출되었음을 눈으로 확인할 수 있습니다.

정말 기쁘게도 ArrayWrapper의 move ctor을 호출해주고 있습니다. 지금 하는 작업은 함수의 반환을 위해 rvalue를 만들고 있다는 점입니다. 근데 여기서 metadata의 move ctor가 호출되지 않는다는 점이 핵심입니다.

어디로 가는지 확인해보면 다음과 같습니다 

move ctor이 아닌 copy ctor이 호출되었습니다. 원인은 위에서 설명했습니다. rvalue reference는 lvalue입니다. 좀 아리송하긴 했으나 잘 보면 맞는 이야기입니다. Callstack을 기준으로 처음 GetArrayWrapper를 호출하면 즉각 _a에 대한 Call by value로 복사가 진행됩니다. 반환은 rvalue로 이 값을 Parameter로 ArrayWrapper A가 받기 때문에 ArrayWrapper A의 Move ctor가 호출됩니다. 하지만 이 parameter를 잘 보면 rvalue를 rvalue referece로 받고 있습니다. 전에 설명했지만 rvalue를 받을 수 있는 인스턴스 중 하나가 mutable rvalue reference입니다. 이 녀석은 lvalue입니다. 즉, other은 rvalue reference이고 metadata의 입장에서는 move ctor가 overload되어 있는 마당에 lvalue에 대해 그것을 호출할 이유가 없습니다. 따라서 _metadata( other._metadata)는 단순히 meta data의 Copy Ctor를 호출하게 된 것입니다.
그렇게 ArrayWrapper에 대해서만 Move Ctor가 호출되고 그 안의 Metadata에 대해서는 Move Ctor가 호출되지 않았던 것입니다. 오류를 찾을 수 없었던 것은 메모리에 접근하여 무언가 작업을 하지 않았던 것이고 때문에 이런 문제가 대단히 위험한 요소로 남아있을 수 있다는 말을 했던 것입니다.
더 쉽고 직관적으로 설명할 수도 있습니다. rvalue는 흔히 말하는 이름이 없습니다. 가령, 10.0f는 단순한 floating point이며 이름이 없습니다. 만일 float x = 100.0f; 라는 식으로 정의되어 있는 x였다면 이는 rvalue가 아닌 lvalue라고 할 수 있습니다. ArrayWrapper는 함수의 호출로 복사가 될 때 이름이 없었습니다. 하지만 ArrayWrapper의 member인 metadata는 어찌되었든 'ArrayWrapper&& other' other이라는 이름을 받았습니다. 마치 위대한 서사시의 한 구절 같습니다. 또 강조해도 부족하지 않은 Both lvalue and rvalue references are lvalue expressions.
std::move
그래서 필요한 것이 <utility>에 정의된 std::move입니다. 이 코드가 놀라운 것은 lvalue를 rvalue로 바꾸어 준다는 점입니다. 함수 이름은 마치 뭔가를 이동시켜줄 것 같이 생겼는데 핵심은 그것이 아니고 Move Constructor를 Invoke할 수 있다는 사실이 핵심입니다. 그래서 이 코드를 Move Constructor를 촉발해야 할 위치에 박아 넣으면 됩니다! 그곳이 어디겠습니까?



마침내!! 마침내 원하는 결과나 나왔습니다. 이것이 Move Semantics에 대한 도입에 해당합니다. 더 깊은 정보가 있긴 합니다. 정말 끝도 없습니다. 이 move가 어떻게 동작하는지 알아보도록 하겠습니다. 그나저나 move assignment는 move ctor와 같은 원리로 만들면 됩니다.
Class Ctor의 성질 중 하나가 우리가 기본 Ctor를 선언하면 Compiler는 더 이상 default ctor를 제공해주지 않는다는 점입니다. 이 성질은 move ctor에도 적용이 되어서 move ctor를 선언을 해버리면 default ctor를 제공하지 않아서 우리가 선언-정의까지 해주어야 합니다. 그런데 move ctor가 default copy ctor을 생성하는 것을 막지는 못하며 move assignment의 선언과 정의가 defualt assignment의 생성을 막지는 못하니 이 점에 꼭 주의를 해야합니다.
std::move()가 동작하는 방법은 상당히 간결합니다. Typecasting을 통해서 lvalue를 rvalue로 바꿔주는 것입니다. 다른 방대한 내용이 있으나 이해가 가능한 부분은 static_cast를 통해서 변환을 한다는 사실입니다.
그러나 결론은 조금 눈물이 납니다. 그래서 이런 Move semantics를 자주 사용해야 하는가? 그렇지는 않다고 합니다. 이는 단순히 Dangling Referece의 발생 가능성을 높일 뿐이라는 것이 결론입니다. 필요한 곳 가령, 어떤 함수의 Process 이후에 반환해야 할 Mesh나 Object를 외부에서 갖고 있는 경우가 있을 겁니다.
Ref
1. https://stackoverflow.com/questions/3413470/what-is-stdmove-and-when-should-it-be-used .
2. https://en.cppreference.com/w/cpp/language/move_constructor .
3. https://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html
'Graphics' 카테고리의 다른 글
[32] - Copy and Filter (0) 2021.08.09 [31] - (C++) For each, Move Semantics, Lambdas, Uniform Initialization - 3 (0) 2021.08.05 [29] - (C++) For each, Move Semantics, Lambdas, Uniform Initialization - 1 (0) 2021.08.03 [28] - Bindable/Drawable System - 2 (0) 2021.08.02 [27] - Bindable/Drawable System (0) 2021.08.01