💬 혹시 std::move 쓰시나요? copy 보다 더 빠르다는 이유로 무분별하게 쓰고 있지 않은 지, copy보다 빠를 것을 기대하고 사용하고 있었으나 실제로 빠르게 동작하고 있나요? 아니면 찜찜해서 아예 사용하지 않고 있나요? 🎯 이번에 제대로 이해하고 올바르게 사용해 봅시다.
📍 오해 풀기 : std::move는 리소스 소유권을 이동시킨다❗ or ❓ 🤔
더 정확히는 std::move 자체가 소유권을 이동시키는 것이 아니라, std::move의 대상이 되는 객체들의 이동 생성자를 호출되게 하는 역할만 하고, 소유권 이동의 유무는 이동 생성자 내부에 즉 객체 설계에 따라 (되고 안되고 가) 결정이 된다.
🔍 move가 어떻게 구현되어 있는지 들여다보자.
move는 단지 r-value 참조로 캐스팅하는 역할만 한다. 자원의 소유권 이동은 그럼 어떻게 되는지 string 클래스를 약식으로 구현한 코드를 통해 알아보자.
string을 유사하게 구현한 예시
#include <iostream>
#include <cstring>
class string
{
char* ptr;
public:
// 생성자
string(const char* s) {}
// 복사 생성자
string(const string& other)
{
// 깊은 복사
ptr = new char[10];
strncpy(ptr, other.ptr, sizeof(ptr));
std::cout << "copy ctor " << std::endl;
}
// 이동 생성자
string(string&& other)
{
// 이 안에서 소유권을 이동시킴
ptr = other.ptr;
other.ptr = nullptr;
std::cout << "move ctor " << std::endl;
}
};
int main()
{
string s1 = "s1 hello";
const string s2 = "s2 hello";
string s3 = std::move(s1); // 자원의 이동
string s4 = std::move(s2); // 자원의 복사
return 0;
}
결과
move ctor
copy ctor
코드 리뷰
string s3 = std::move(s1);
move는 string s1을 r-value로 캐스팅하여 s3에 대입하는 코드이다.
실행결과 로그에서 보시다시피("move ctor"), r-value로 캐스팅된 변수를 받는 이동생성자 string(string&& other)가 정의되어 있어 해당 코드 내로 진입을 하고 이 코드 안에서 s1이 할당되어 있는 힙메모리는 그대로 둔 채 포인터만 s3로 이동시키고 s1은 nullptr로 세팅하여 자원의 이동이 되었다. 이처럼 move를 통해 소유권이 이동되는 방식은 이동 생성자 안에서 자원을 이동시켰기에 이동이 된 것이지 이동 생성자 내부에 아무런 코드가 없다면 자원은 이동되지 않는다.
const string s2 = "s2 hello";
string s4 = std::move(s2);
s2는 s1과 다르게 상수 스트링인데 move를 하면 "copy ctor" 이 호출된다. 다시 말해 move를 했음에도 자원의 이동이 안된다. 이 코드는 아래코드 표현할 수 있는데 move는 명시적으로 타입을 전하지 않으므로 컴파일러가 추론하여
static_cast<const std::string&&>(s2); //원본
static_cast<const std::string&>(s2); //추론
상수 참조 인자를 가지는 복사생성자가 호출된다. (그래서 출력결과가 copy ctor이 된 것이다.)
string(const string& other) {} <= 이 함수 호출!
복사 생성자 안에서는 깊은 복사를 하게끔 설계되어 있기에 move를 사용했음에도 깊은 복사가 되었다.
📝 정리하면 이처럼 std::move는 r-value로 캐스팅할 뿐 자원의 소유권 이동 유무는 객체의 설계에 따라 결정된다. 📌그리고 개념적으로도 상수는 move를 할 수 없다. 자원의 소유권을 이동시키기 위해서는 포인터 혹은 참조를 변경해야 하는데 상수는 변경이 불가하기 때문이다. 💬 생각해 보면 당연한 얘기이다.
📜 History
C++98 문법에서는 r-value가 없었기에 상수 참조 "foo(const T& n)" 형태가 최선이었다고 한다. (무조건 복사)
C++11 이후에는 std::move 지원이 가능한 최선의 코드는 아래와 같다.
void foo(const T& n){ m_n = n;}; // 자원 복사
void foo( T&& n){ m_n = std::move(n);}; // 자원 이동
💬 C++핵심 가이드라인에서 X&&타입의 경우는 std::move로 전달하라는 이유는 여기에 있는 것이다.
그 외 다른 사용 예
#include <iostream>
#include <vector>
int main()
{
std::vector<std::string> v;
std::string s1 = "s1 hello";
std::string s2 = "s2 hello";
v.push_back(s1); // s1 자원 복사
v.push_back(std::move(s2)); // s2 자원 이동
}
참고
* 본 글에 예제 코드는 코드누리 교육을 받으면서 제공된 샘플코드를 활용하였습니다.
📌 Rust vs C++ copy, move 콘셉트 비교
. C++ : 디폴트는 copy, move 하려면, std::move 사용
- std::string s1 = s2; //copy
- std::string s4 = std::move(s3); //move
. RUST : 디폴트가 move, copy 하려면, .clone() 사용
- String s2 = s1; // move
- String s4 = s3.clone(); //copy
💬 Rust가 기본적으로 move 개념으로 사용하는 것만 봐도 Rust 언어의 콘셉트가 성능 향상을 중요하게 생각하는 철학이 있음을 엿볼 수 있다.
C++ 핵심 가이드라인 한글화 프로젝트 링크
C++ Core Guidelines 원문 링크
728x90
'L C++' 카테고리의 다른 글
(응용) 생성자(constructors) 초기화 (16) | 2024.04.21 |
---|---|
(기초) 생성자(constructors) 초기화 (1) | 2024.04.17 |
perfect forwarding을 perfect하게 이해하기 (14) | 2024.03.29 |
std::string_view의 이해 (0) | 2024.03.19 |
[C++ Core Guidelines] 매개변수 전달 표현식 규칙 in Functions (4) | 2024.02.27 |