복사 및 이동 의미론 (Copy and Move Semantics)
개요
C++에서 객체를 다룰 때, 객체의 데이터를 복사하거나 소유권을 이전하는 방식은 매우 중요합니다. 특히 동적 할당된 메모리와 같은 리소스를 관리하는 클래스의 경우, 이러한 동작을 어떻게 정의하느냐에 따라 프로그램의 성능과 안정성이 크게 달라집니다.
- 복사 의미론(Copy Semantics): 기존 객체의 내용을 그대로 복사하여 새로운 객체를 생성하거나 다른 객체에 대입합니다. 원본과 사본은 독립적인 리소스를 가집니다.
- 이동 의미론(Move Semantics): 기존 객체(주로 임시 객체)의 리소스 소유권을 새로운 객체로 '이동'시킵니다. 불필요한 데이터 복사를 피할 수 있어 성능이 크게 향상됩니다. 원본 객체는 리소스를 잃고 비어 있는(valid but unspecified) 상태가 됩니다.
이러한 동작은 컴파일러가 자동으로 생성하는 특별 멤버 함수들에 의해 제어되며, 필요에 따라 사용자가 직접 정의할 수 있습니다. 이를 'Rule of Three', 'Rule of Five', 'Rule of Zero'와 같은 규칙으로 설명하기도 합니다.
C++ 버전별 주요 키워드 도입 시기
- C++98/03: 복사 생성자, 복사 대입 연산자, 소멸자를 중심으로 한 'Rule of Three'가 일반적이었습니다.
- C++11: Rvalue 참조(
&&
)와std::move
가 도입되면서 이동 의미론이 표준화되었습니다. 이로 인해 'Rule of Five'라는 개념이 등장했습니다.
내용 설명
The Rule of Five (5의 규칙)
클래스가 리소스를 관리할 때, 다음 다섯 가지 특별 멤버 함수 중 하나를 사용자가 직접 정의해야 한다면, 나머지 네 가지도 모두 직접 정의하거나 = default
, = delete
를 이용해 컴파일러의 동작을 명시적으로 제어해야 한다는 지침입니다.
- 소멸자 (Destructor):
~Matrix()
- 객체가 소멸될 때 호출됩니다. 주로 동적 할당된 메모리 해제와 같은 리소스 정리 작업을 수행합니다.
- 복사 생성자 (Copy Constructor):
Matrix(const Matrix &other)
- 같은 타입의 다른 객체를 인자로 받아 새로운 객체를 생성할 때 호출됩니다. (예:
Matrix m2 = m1;
) - 깊은 복사(deep copy)를 통해 원본과 완전히 독립된 사본을 만듭니다.
- 같은 타입의 다른 객체를 인자로 받아 새로운 객체를 생성할 때 호출됩니다. (예:
- 복사 대입 연산자 (Copy Assignment Operator):
Matrix &operator=(const Matrix &other)
- 이미 생성된 객체에 다른 객체의 값을 대입할 때 호출됩니다. (예:
m2 = m3;
) - 자기 자신에게 대입하는 경우(
this != &other
)를 확인하고, 기존 리소스를 정리한 후 새로운 리소스를 복사합니다.
- 이미 생성된 객체에 다른 객체의 값을 대입할 때 호출됩니다. (예:
- 이동 생성자 (Move Constructor):
Matrix(Matrix &&other) noexcept
- 임시 객체(rvalue)를 사용해 새로운 객체를 생성할 때 호출됩니다.
- 원본 객체의 리소스(포인터 등)를 그대로 가져와 자신의 멤버로 설정하고, 원본 객체는 리소스를 잃었음을 표시합니다(예:
nullptr
로 설정). - 리소스 포인터만 복사하므로 매우 빠릅니다.
noexcept
키워드는 이 연산이 예외를 던지지 않음을 명시하여, STL 컨테이너 등이 더 효율적으로 이 함수를 사용할 수 있게 합니다.
- 이동 대입 연산자 (Move Assignment Operator):
Matrix &operator=(Matrix &&other) noexcept
- 이미 생성된 객체에 임시 객체의 값을 대입할 때 호출됩니다.
- 자기 자신에게 대입하는 경우를 확인하고, 기존 리소스를 정리한 후 원본의 리소스를 '훔쳐옵니다'.
Matrix
예제에서는 std::cout
을 통해 각 함수가 언제 호출되는지 명확히 보여줍니다.
예제 코드
Matrix.h
#pragma once
#include <iostream>
#include <utility> // for std::move
template <typename T>
class Matrix
{
public:
Matrix();
Matrix(const T &A, const T &B, const T &C, const T &D);
/****************/
/* RULE OF FIVE */
/****************/
~Matrix() noexcept;
Matrix(const Matrix &other); // copy constructor
Matrix &operator=(const Matrix &other); // copy assignment operator
Matrix(Matrix &&other) noexcept; // move constructor
Matrix &operator=(Matrix &&other) noexcept; // move assignment operator;
Matrix operator+(const Matrix &rhs);
Matrix &operator+=(const Matrix &rhs);
Matrix operator-(const Matrix &rhs);
Matrix &operator-=(const Matrix &rhs);
void print_matrix() const;
T get_A() const;
T get_B() const;
T get_C() const;
T get_D() const;
void set_A(const T &new_A);
void set_B(const T &new_B);
void set_C(const T &new_C);
void set_D(const T &new_D);
private:
T m_A;
T m_B;
T m_C;
T m_D;
};
template <typename T>
Matrix<T>::Matrix() : m_A(0.0), m_B(0.0), m_C(0.0), m_D(0.0)
{
std::cout << "Calling Cstr\n";
}
template <typename T>
Matrix<T>::Matrix(const T &A, const T &B, const T &C, const T &D)
: m_A(A), m_B(B), m_C(C), m_D(D)
{
std::cout << "Calling Cstr\n";
}
template <typename T>
Matrix<T>::~Matrix() noexcept
{
std::cout << "Calling Dstr\n";
}
template <typename T>
Matrix<T>::Matrix(const Matrix<T> &other)
: m_A(other.get_A()), m_B(other.get_B()), m_C(other.get_C()),
m_D(other.get_D())
{
std::cout << "Copy constructor\n";
}
template <typename T>
Matrix<T> &Matrix<T>::operator=(const Matrix<T> &other)
{
if (this != &other)
{
m_A = other.get_A();
m_B = other.get_B();
m_C = other.get_C();
m_D = other.get_D();
}
std::cout << "Copy assignment\n";
return *this;
}
template <typename T>
Matrix<T>::Matrix(Matrix<T> &&other) noexcept
: m_A(std::move(other.m_A)), m_B(std::move(other.m_B)),
m_C(std::move(other.m_C)), m_D(std::move(other.m_D))
{
// 원본 객체의 멤버들을 기본값으로 초기화하여 리소스 소유권이 이전되었음을 명확히 함
other.m_A = T{};
other.m_B = T{};
other.m_C = T{};
other.m_D = T{};
std::cout << "Move constructor\n";
}
template <typename T>
Matrix<T> &Matrix<T>::operator=(Matrix<T> &&other) noexcept
{
if (this != &other)
{
m_A = std::move(other.m_A);
m_B = std::move(other.m_B);
m_C = std::move(other.m_C);
m_D = std::move(other.m_D);
other.m_A = T{};
other.m_B = T{};
other.m_C = T{};
other.m_D = T{};
}
std::cout << "Move assignment\n";
return *this;
}
template <typename T>
Matrix<T> Matrix<T>::operator+(const Matrix<T> &rhs)
{
auto result = Matrix{};
result.set_A(this->get_A() + rhs.get_A());
result.set_B(this->get_B() + rhs.get_B());
result.set_C(this->get_C() + rhs.get_C());
result.set_D(this->get_D() + rhs.get_D());
return result;
}
template <typename T>
Matrix<T> &Matrix<T>::operator+=(const Matrix<T> &rhs)
{
this->set_A(this->get_A() + rhs.get_A());
this->set_B(this->get_B() + rhs.get_B());
this->set_C(this->get_C() + rhs.get_C());
this->set_D(this->get_D() + rhs.get_D());
return *this;
}
template <typename T>
Matrix<T> Matrix<T>::operator-(const Matrix<T> &rhs)
{
auto result = Matrix{};
result.set_A(get_A() - rhs.get_A());
result.set_B(get_B() - rhs.get_B());
result.set_C(get_C() - rhs.get_C());
result.set_D(get_D() - rhs.get_D());
return result;
}
template <typename T>
Matrix<T> &Matrix<T>::operator-=(const Matrix<T> &rhs)
{
set_A(get_A() - rhs.get_A());
set_B(get_B() - rhs.get_B());
set_C(get_C() - rhs.get_C());
set_D(get_D() - rhs.get_D());
return *this;
}
template <typename T>
void Matrix<T>::print_matrix() const
{
std::cout << m_A << " " << m_B << '\n';
std::cout << m_C << " " << m_D << "\n\n";
}
template <typename T>
T Matrix<T>::get_A() const { return m_A; }
template <typename T>
T Matrix<T>::get_B() const { return m_B; }
template <typename T>
T Matrix<T>::get_C() const { return m_C; }
template <typename T>
T Matrix<T>::get_D() const { return m_D; }
template <typename T>
void Matrix<T>::set_A(const T &new_A) { m_A = new_A; }
template <typename T>
void Matrix<T>::set_B(const T &new_B) { m_B = new_B; }
template <typename T>
void Matrix<T>::set_C(const T &new_C) { m_C = new_C; }
template <typename T>
void Matrix<T>::set_D(const T &new_D) { m_D = new_D; }
main.cpp
#include <iostream>
#include <vector>
#include "Matrix.h"
int main()
{
// // Copy Example
// auto m1 = Matrix<float>{-1.0, -2.0, -3.0, -4.0};
// auto m2 = m1; // copy constructor
// auto m3 = Matrix<float>{1.0, -2.0, -3.0, -4.0};
// m2 = m3; // copy assignm operator
// Move Example
auto vec = std::vector<Matrix<float>>{};
// 1. 임시 Matrix 객체(rvalue) 생성
// 2. push_back은 이 rvalue를 받아 vector 내부에서 이동 생성자를 호출하여 새 원소를 생성
vec.push_back(Matrix<float>{-1.0, -2.0, -3.0, -4.0});
vec[0].print_matrix();
return 0;
}
실행 결과
Calling Cstr
Move constructor
Calling Dstr
-1 -2
-3 -4
Calling Dstr
결과 분석:
Calling Cstr
:Matrix<float>{-1.0, ...}
에 의해 임시 객체가 생성되며 일반 생성자가 호출됩니다.Move constructor
:vec.push_back()
이 임시 객체(rvalue)를 인자로 받습니다.vector
는 내부 공간에 새로운Matrix
객체를 생성하기 위해 복사 대신 이동 생성자를 호출하여 리소스를 '훔쳐옵니다'.Calling Dstr
:push_back
호출이 끝난 후, 리소스를 빼앗긴 임시 객체가 소멸됩니다.-1 -2 ...
:vec[0].print_matrix()
가 호출되어vector
내부에 저장된 객체의 내용이 올바르게 출력됩니다.Calling Dstr
:main
함수가 종료되면서vector
가 소멸되고,vector
가 관리하던Matrix
객체도 소멸됩니다.
활용팁
- 성능 최적화: 동적 메모리, 파일 핸들러, 소켓 등 무거운 리소스를 관리하는 클래스에서 이동 의미론을 구현하면 불필요한 깊은 복사를 방지하여 성능을 크게 향상시킬 수 있습니다.
std::vector
,std::string
,std::unique_ptr
와 같은 표준 라이브러리 컨테이너와 스마트 포인터들은 이미 이동 의미론을 완벽하게 지원합니다. std::move
의 사용:std::move
는 객체를 rvalue로 캐스팅하는 역할을 합니다. 이를 통해 lvalue(이름이 있는 객체)에 대해서도 강제로 이동 시킬 수 있습니다. 단,std::move
를 사용한 후에는 원본 객체가 비어 있는 상태가 되므로 더 이상 사용하면 안 됩니다.- 함수 반환: 함수에서 객체를 값으로 반환할 때, 컴파일러는 RVO(Return Value Optimization)나 NRVO(Named Return Value Optimization)를 통해 불필요한 복사/이동을 생략하는 최적화를 수행합니다. 이것이 불가능한 경우, 이동 생성자가 호출되어 효율적으로 값을 반환합니다.
'개발 > C++ (98,03,11,14,17,20,23)' 카테고리의 다른 글
Modern C++ : 연산자 오버로딩 (Operator Overloading) (98, 11, 20) (0) | 2025.09.21 |
---|---|
Modern C++ : 클래스 템플릿(Class Template) (98) (0) | 2025.09.20 |
Modern C++ : 다형성(Polymorphism) (98, 11) (0) | 2025.09.19 |
Modern C++ : 상속(Inheritance) (98, 11) (0) | 2025.09.18 |
Modern C++ : 클래스(Class) (98, 11, 17) (0) | 2025.09.17 |
Modern C++ : 유틸리티 함수 활용 (11, 17) (1) | 2025.09.15 |
Modern C++ : 직접 구현한 표준 알고리즘, equal, fill_n, iota, copy, accumulate (98, 11) (0) | 2025.09.14 |
Modern C++ : std::function (11) (0) | 2025.09.13 |