SW 그리고 아빠 엔지니어링 중...

아는 만큼 보이고, 경험해봐야 알 수 있고, 자꾸 써야 내 것이 된다.

L C++

std::move the 이해하기

보리남편 김 주부 2024. 4. 12. 01:51
💬 혹시 std::move 쓰시나요? copy 보다 더 빠르다는 이유로 무분별하게 쓰고 있지 않은 지, copy보다 빠를 것을 기대하고 사용하고 있었으나 실제로 빠르게 동작하고 있나요? 아니면 찜찜해서 아예 사용하지 않고 있나요? 🎯 이번에 제대로 이해하고 올바르게 사용해 봅시다.

 

📍 오해 풀기 : std::move는 리소스 소유권을 이동시킨다❗ or ❓ 🤔

더 정확히는 std::move 자체가 소유권을 이동시키는 것이 아니라, std::move의 대상이 되는 객체들의 이동 생성자를 호출되게 하는 역할만 하고, 소유권 이동의 유무는 이동 생성자 내부에 즉 객체 설계에 따라 (되고 안되고 가) 결정이 된다. 

 

🔍 move가 어떻게 구현되어 있는지 들여다보자.

 

std::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