C++11에 들어서서 rvalue (Right-value)에 대한 기능(?)들이 생기기 시작 했었습니다.
그 중에서 이동 생성자(Move Constructors)
와 이동 할당 연산자(Move Assignment Operators)
에 대한 이야기 입니다.
기본 레퍼런스 내용
지금부터 나올 이야기는 위의 MS Learn(구 MSDN)에 나온 이야기와 기타 공부를 했었던 내용의 짬뽕입니다.
rvalue (Right-value)와 lvalue (Left-value)
이 이야기를 다루기 위해서는 rvalue
와 lvalue
에 대해서 먼저 이야기해야 합니다.
통상적으로 Right-value
를 뜻하는 rvalue
는 리터럴(Literal)
혹은 임시 객체(Temporary Object)
라고 불리우는 그것 입니다.
즉, 쉽게 말해서 주소값, 레퍼런스화 할 수 없는 값
입니다.
int nNum = 33; // 33은 rvalue
이렇게 흔히 Right-side에 오는 값이어서 Right-value
라고 불리웁니다.
하지만 위에서 말했듯이 오른쪽에 오는 리터럴(Literal)
혹은 임시 객체(Temporary Object)
에만 해당하는 값만 rvalue
라고 합니다.
그럼 lvalue
는 rvalue
의 반대 개념이겠죠?
즉, 주소값을 취할 수 있는, 레퍼런스화 할 수 있는 값
을 말합니다.
int nNum = 33; // nNum은 lvalue
int& nlNum = nNum; // nNum은 lvalue임으로 레퍼런스를 취할 수 있음
그렇기 때문에 이렇게 위의 예제에서 nlNum
이라는 레퍼런스 변수에 대입할 수 있습니다.
이렇게 Left-side에 오는 값이어서 Left-value
라고 불립니다.
하지만 lvalue
가 Right-side에 간다고 해서 rvalue
가 되지는 못합니다.
(그 이유는 rvalue에서 설명 했습니다.)
이동 생성자 (Move Constructors)
이동 생성자의 경우 rvalue 참조 선언자(&&)
를 사용합니다.
class MyClass
{
public:
/**
* @brief Copy constructor
* @param cls 복사 대상인 데이터
*/
MyClass(const MyClass& cls)
{
m_nLength = cls.m_nLength;
m_pszName = new char[m_nLength + 1];
::strcpy_s(m_pszName, m_nLength + 1, cls.m_pszName);
std::cout << "복사 생성자: " << m_pszName << std::endl;
}
/**
* @brief Move constructor (noexcept를 선언해주어야 함)
* @param cls 이동 대상인 데이터 (rvalue 참조 선언자)
*/
MyClass(MyClass&& cls) noexcept
{
m_pszName = cls.m_pszName;
m_nLength = cls.m_nLength;
cls.m_nLength = 0;
cls.m_pszName = nullptr; // 제거하지 못하도록 nullptr로 바꾸어버림
std::cout << "이동 생성자: " << m_pszName << std::endl;
}
/**
* @brief Deconstructor
*/
~MyClass() noexcept
{
delete[] m_pszName;
}
/* 생략 */
private:
int m_nLength; // 문자열 데이터의 길이 (\0 제외)
char* m_pszName; // 문자열 데이터
};
이동 생성자의 경우 rvalue 참조 선언자(&&)
로 이동할 인자를 받고 내부에서는 이동하도록 코드를 짜줍니다.
위의 경우에는 이동할 데이터인 cls가 현재 생성되는 데이터로 문자열 데이터(m_pszName)을 넘겨주고 cls 내부의 문자열 데이터(m_pszName)을 nullptr로 바꾸어 줌으로써 cls가 소멸할 때 이동한 데이터를 제거하지 못하도록 합니다.
또한 해당 이동 생성자의 경우 noexcept
이어야만 합니다.
이동 할당 연산자(Move Assignment Operators)
이동 할당 연산자의 경우에도 이동 생성자와 유사합니다.
/**
* @brief Move assignment operator
* @param cls 이동 대상인 데이터
* @return
*/
MyClass& operator=(MyClass&& cls) noexcept
{
if (this != &cls)
{
delete[] m_pszName;
m_pszName = cls.m_pszName;
m_nLength = cls.m_nLength;
cls.m_nLength = 0;
cls.m_pszName = nullptr; // 제거하지 못하도록 nullptr로 바꾸어버림
}
return *this;
}
이동 할당 연산자에도 rvalue 참조 선언자(&&)
로 선언되며 이 또한 noexcept
이어야만 합니다.
여기까지의 예제
여기까지 만든 MyClass
는 아래와 같습니다.
#include <iostream>
#include <string>
class MyClass
{
public:
/**
* @brief Copy constructor (문자열 복사)
* @param pszName 복사할 문자열
*/
MyClass(const char* pszName)
{
m_nLength = static_cast<int>(::strlen(pszName));
m_pszName = new char[m_nLength + 1];
::strcpy_s(m_pszName, m_nLength + 1, pszName);
std::cout << "문자열 복사 생성자: " << m_pszName << std::endl;
}
/**
* @brief Move constructor (noexcept를 선언해주어야 함)
* @param cls 이동 대상인 데이터 (rvalue 참조 선언자)
*/
MyClass(MyClass&& cls) noexcept
{
m_pszName = cls.m_pszName;
m_nLength = cls.m_nLength;
cls.m_nLength = 0;
cls.m_pszName = nullptr;
std::cout << "이동 생성자: " << m_pszName << std::endl;
}
/**
* @brief Copy constructor
* @param cls 복사 대상인 데이터
*/
MyClass(const MyClass& cls)
{
m_nLength = cls.m_nLength;
m_pszName = new char[m_nLength + 1];
::strcpy_s(m_pszName, m_nLength + 1, cls.m_pszName);
std::cout << "복사 생성자: " << m_pszName << std::endl;
}
/**
* @brief Deconstructor
*/
~MyClass()
{
delete[] m_pszName;
m_pszName = nullptr;
}
/**
* @brief Move assignment operator
* @param cls 이동 대상인 데이터
* @return
*/
MyClass& operator=(MyClass&& cls) noexcept
{
if (this != &cls)
{
delete[] m_pszName;
m_pszName = cls.m_pszName;
m_nLength = cls.m_nLength;
cls.m_nLength = 0;
cls.m_pszName = nullptr;
}
return *this;
}
private:
int m_nLength; // 문자열 길이
char* m_pszName; // 문자열 데이터 (new 하는 데이터로 소멸자에서 제거 예정)
};
int main(void)
{
MyClass cls1("Hello"); // 문자열 복사 생성자
MyClass clsCopy = cls1; // 복사 생성자
MyClass clsMove = std::move(cls1); // 이동 생성자
return 0;
}
자동으로 생성되는 이동 생성자 및 이동 할당 연산자
그럼 항상 이동 생성자와 이동 할당 연산자를 구현해야 할까요?
If a class doesn't define a move constructor, the compiler generates an implicit one if there's no user-declared copy constructor, copy assignment operator, move assignment operator, or destructor. If no explicit or implicit move constructor is defined, operations that would otherwise use a move constructor use the copy constructor instead. If a class declares a move constructor or move assignment operator, the implicitly declared copy constructor is defined as deleted.
출처: https://learn.microsoft.com/en-us/cpp/cpp/constructors-cpp?view=msvc-170#move_constructors
위의 내용을 보면 클래스가 이동 생성자를 정의하지 않았으며 사용자가 복사 생성자, 복사 할당 연산자, 이동 할당 연산자 또는 소멸자를 선언하지 않은 경우 경우 암시적 생성자를 생성한다고 되어있습니다.
MyClass(const MyClass& other); // 복사 생성자
MyClass& operator=(const MyClass& other); // 복사 할당 연산자
MyClass(MyClass&& other); // 이동 생성자
MyClass& operator=(MyClass&& other); // 이동 할당 연산자
~MyClass(); // 소멸자
즉, 위와 같이 선언하지 않았다면 자동으로 생성을 해줍니다.
추가적으로 참조형 변수가 있는 경우에 자동으로 생성해주지 않습니다.
A defaulted copy/move assignment operator for class X is defined as deleted if X has:
- a variant member with a non-trivial corresponding assignment operator and X is a union-like class, or
- a non-static data member of const non-class type (or array thereof), or
- a non-static data member of reference type, or
- a non-static data member of class type M (or array thereof) that cannot be copied/moved because overload resolution (13.3), as applied to M’s corresponding assignment operator, results in an ambiguity or a function that is deleted or inaccessible from the defaulted assignment operator, or
- a direct or virtual base class B that cannot be copied/moved because overload resolution (13.3), as applied to B’s corresponding assignment operator, results in an ambiguity or a function that is deleted or inaccessible from the defaulted assignment operator, or
- for the move assignment operator, a non-static data member or direct base class with a type that does not have a move assignment operator and is not trivially copyable, or any direct or indirect virtual base class.
출처: C++ International Standard N3242(C++11) 12.8 Copying and moving class objects, 24번
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf
이동과 같이 사용하는 함수) std::move 함수
위에서 봤던 여기까지의 예제 에서 main 함수
에서 std::move
라는 함수를 봤을 겁니다.
std::move 함수
를 썼을 때는 이동 생성자로, 그렇게 하지 않았을 때는 복사 생성자로 유도되는 것을 볼 수 있습니다.
std::move 함수
는 해당 object가 이동할 수 있음을 암시만 합니다.
즉, 이동 생성자 또는 이동 할당 연산자가 선언되어 있지 않다면 이동이 아닌 복사를 수행할 수도 있다는 겁니다.
무조건 이동을 보장하지는 않는다는 것이지요.
그래서 위의 MyClass
에서 이동 생성자와 이동 할당 연산자를 제거하면 std::move함수
를 호출해도 복사만 수행하게 됩니다.
#include <iostream>
#include <string>
class MyClass
{
public:
/**
* @brief Copy constructor (문자열 복사)
* @param pszName 복사할 문자열
*/
MyClass(const char* pszName)
{
m_nLength = static_cast<int>(::strlen(pszName));
m_pszName = new char[m_nLength + 1];
::strcpy_s(m_pszName, m_nLength + 1, pszName);
std::cout << "문자열 복사 생성자: " << m_pszName << std::endl;
}
/**
* @brief Copy constructor
* @param cls 복사 대상인 데이터
*/
MyClass(const MyClass& cls)
{
m_nLength = cls.m_nLength;
m_pszName = new char[m_nLength + 1];
::strcpy_s(m_pszName, m_nLength + 1, cls.m_pszName);
std::cout << "복사 생성자: " << m_pszName << std::endl;
}
/**
* @brief Deconstructor
*/
~MyClass()
{
delete[] m_pszName;
m_pszName = nullptr;
}
void Release()
{
delete[] m_pszName;
m_pszName = nullptr;
}
private:
int m_nLength; // 문자열 길이
char* m_pszName; // 문자열 데이터 (new 하는 데이터로 소멸자에서 제거 예정)
};
int main(void)
{
MyClass cls1("Hello"); // 문자열 복사 생성자
MyClass clsCopy = cls1; // 복사 생성자
MyClass clsMove = std::move(cls1); // 복사 생성자 (이동 생성자가 없으므로)
return 0;
}
'Study > C++' 카테고리의 다른 글
[C++/Console] 테트리스 만들어 보기 - 7 (자동 하강, 블럭 회전, 라인 클리어) (1) | 2023.04.17 |
---|---|
[C++11] unordered_map (0) | 2023.03.20 |
[C++/Console] 테트리스 만들어 보기 - 6 (바닥 충돌, 블럭 랜덤 생성) (0) | 2023.03.06 |
[C++/Console] 테트리스 만들어 보기 - 5 (충돌 판정 - 벽, 블럭) (0) | 2023.02.20 |
[C++/Console] 테트리스 만들어 보기 - 4 (플레이어(블럭) 움직임) (0) | 2023.02.09 |