4. 이동가능 객체(Movable Type)
클래스를 이동 가능하게 만들면 많은 장점들이 있고 이동 가능한 사용자 정의 타입은 표준 라이브러리 유틸과도 잘 동작한다.
이동 연산을 지원하는 클래스를 만드는 경우 코드의 성능, 안정성이나 구현 할 수 있는 방식이 증가한다.
이동 가능한 사용자 타입을 정의하게 되면 표준 라이브리를 사용하는 중에도 이동 연산에 대한 이점을 얻을 수 가 있다.
C++11 이전에 컴파일러는 기본 생성자, 소멸자, 복사 생성자, 복사 할당 연산자를 자동으로 추가해 주었는데, C++11 이 후는 여기에 이동 생성자와 이동 할당 연산자가 추가되었다.
여기서는 유리수를 나타내는 Rational 클래스를 통해서 이동 연산이 추가되었을 때 어떤 장점이 있는지 살펴본다.
#pragma once
#include <iostream>
class Rational {
public:
Rational();
Rational(int numerator, int denominator = 1);
int numerator() const { return numerator_; }
int denominator() const { return denominator_; }
Rational(const Rational& rhs); // Copy Constructor
Rational& operator = (const Rational& rhs); //Copy Assignment
friend std::ostream& operator << (std::ostream& oss, const Rational& r);
private:
void simplify();
private:
int numerator_;
int denominator_;
};
Rational.cpp
#include "Rational.h"
#include <vector>
#include <cmath>
Rational::Rational(): numerator_(0), denominator_(1){
}
Rational::Rational(int numerator, int denominator) : numerator_(numerator),
denominator_(denominator){
simplify();
}
Rational::Rational(const Rational& rhs) {
std::cout << "Copy Construct" << std::endl;
numerator_ = rhs.numerator_;
denominator_ = rhs.denominator_;
}
Rational& Rational::operator = (const Rational& rhs) {
std::cout << "Assignment" << std::endl;
numerator_ = rhs.numerator_;
denominator_ = rhs.denominator_;
return *this;
}
std::ostream& operator << (std::ostream& oss, const Rational& r) {
return oss << r.numerator_ << "/" << r.denominator_;
}
void Rational::simplify() {
}
위의 Rational은 복사 연산자를 구현했으므로 컴파일러가 자동으로 이동 연산자를 생성해 주지 않는다.
#include "Rational.h"
#include <vector>
int main() {
Rational r0(2, 1);
Rational r1(2, 2);
std::vector<Rational> rationals;
rationals.reserve(10);
rationals.push_back(r0); //왼값
rationals.push_back(Rational(2, 3)); //오른값
rationals.push_back(std::move(r0)); //오른값
std::cout << r0 << std::endl;
return 0;
}
이동 가능하지 않은 클래스를 벡터에 추가하는 경우 push_back 함수에 전달된 인자가 오른값 / 왼값에 상관없이 모두 복사해서 새로운 객체를 생성한 후 벡터에 추가된다.

이제 이동 생성자와 이동 할당 연산자를 Rational 클래스에 추가해 결과가 어떻게 변화는지를 살펴보자..
class Rational { public: .... Rational(Rational&& rhs); //이동 생성자 Rational& operator = (Ratinal&& rhs); //이동 할당자 ... } Rational::Rational(Rational&& rhs) { std::cout << "Move Construct" << std::endl; numerator_ = std::exchange(rhs.numerator_, 0); denominator_ = std::exchange(rhs.denominator_, 1); } Rational& Rational::operator = (Rational&& rhs) { std::cout << "Move Assignment" << std::endl; numerator_ = std::exchange(rhs.numerator_, 0); denominator_ = std::exchange(rhs.denominator_, 1); return *this; } |
이동 가능하도록 구현한 Rational 객체를 위와 동일하게 벡터에 추가하는 경우 결과는 아래와 같다.

위에 살펴본 것 처럼 개발자가 이동가능하게 객체를 설계하게되면 표준 라이브러리를 사용하는 데 있어 이동 연산으로 인해 복사를 피하면서 취할 수 있는 여러 이점들이 있다.
전체 구현은 아래와 같다.
Rational.h
#pragma once
#include <iostream>
class Rational {
public:
Rational();
Rational(int numerator, int denominator = 1);
int numerator() const { return numerator_; }
int denominator() const { return denominator_; }
Rational(const Rational& rhs); // Copy Constructor
Rational& operator = (const Rational& rhs); //Copy Assignment
Rational(Rational&& rhs); //Move Constructor
Rational& operator = (Rational&& rhs); // Move Assignment
friend std::ostream& operator << (std::ostream& oss, const Rational& r);
private:
void simplify();
private:
int numerator_;
int denominator_;
};
Rational.cpp
#include "Rational.h"
#include <vector>
#include <cmath>
void simplify(int& numenrator, int& denominator);
Rational::Rational(): numerator_(0), denominator_(1){
}
Rational::Rational(int numerator, int denominator) : numerator_(numerator),
denominator_(denominator){
simplify();
}
Rational::Rational(const Rational& rhs) {
std::cout << "Copy Construct" << std::endl;
numerator_ = rhs.numerator_;
denominator_ = rhs.denominator_;
}
Rational& Rational::operator = (const Rational& rhs) {
std::cout << "Assignment" << std::endl;
numerator_ = rhs.numerator_;
denominator_ = rhs.denominator_;
return *this;
}
Rational::Rational(Rational &&rhs) {
std::cout << "Move Construct" << std::endl;
numerator_ = std::exchange(rhs.numerator_, 0);
denominator_ = std::exchange(rhs.denominator_, 1);
}
Rational & Rational::operator=(Rational &&rhs) {
std::cout << "Move Assignment" << std::endl;
numerator_ = std::exchange(rhs.numerator_, 0);
denominator_ = std::exchange(rhs.denominator_, 1);
return *this;
}
std::ostream& operator << (std::ostream& oss, const Rational& r) {
return oss << r.numerator_ << "/" << r.denominator_;
}
void Rational::simplify() {
int temp_n = numerator_;
int temp_d = denominator_;
::simplify(temp_n, temp_d);
numerator_ = temp_n;
denominator_ = temp_d;
}
void get_factors(int num, std::vector<int>& factor_set) {
if(num != 1) {
factor_set.push_back(num);
}
for(int i = 2; i <= std::sqrt(static_cast<double>(num)); i++) {
if(num % i == 0) {
factor_set.push_back(i);
factor_set.push_back(num/i);
}
}
}
void simplify(int& numenrator, int& denominator) {
int temp_n = numenrator;
int temp_d = denominator;
int small, temp;
std::vector<int> factor_set;
if(temp_n == temp_d) {
numenrator = 1;
denominator = 1;
return;
}
if(temp_n == -temp_d) {
numenrator = -1;
denominator = 1;
return;
}
if(temp_n == 0){
denominator = 1;
return;
}
small = std::abs(temp_n) < std::abs(temp_d)? std::abs(temp_n) : std::abs(temp_d);
get_factors(small, factor_set);
for(size_t i =0; i < factor_set.size(); i++) {
temp = factor_set[i];
while(temp_n % temp == 0 && temp_d % temp == 0) {
temp_n /= temp;
temp_d /= temp;
}
}
numenrator = temp_n;
denominator = temp_d;
}
main.cpp
#include "Rational.h"
#include <vector>
int main() {
Rational r0( 10, 5);
Rational r1(2, 2);
std::vector<Rational> rationals;
rationals.reserve(10);
rationals.push_back(r0);
rationals.push_back(Rational(2, 3));
rationals.push_back(std::move(r0));
std::cout << r0 << std::endl;
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(movable_rational)
set(CMAKE_CXX_STANDARD 17)
add_executable(movable_rational main.cpp Rational.cpp Rational.h)