Modern C++/Move Semantics

4. 이동가능 객체(Movable Type)

basker 2020. 6. 30. 01:15

클래스를 이동 가능하게 만들면 많은 장점들이 있고 이동 가능한 사용자 정의 타입은 표준 라이브러리 유틸과도 잘 동작한다. 

이동 연산을 지원하는 클래스를 만드는 경우 코드의 성능, 안정성이나 구현 할 수 있는 방식이 증가한다.

이동 가능한 사용자 타입을 정의하게 되면 표준 라이브리를 사용하는 중에도 이동 연산에 대한 이점을 얻을 수 가 있다.

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)