ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C++] 깊은 복사와 얕은 복사
    C++/이것이 C++이다 2021. 3. 30. 03:43

    깊은 복사(Deep copy), 얕은 복사(Shallow copy)에 대한 개념은

    값에 의한 호출(Call by value)참조에 의한 호출(Call by reference)와 깊은 관련이 있습니다.

    대표적으로 전달받은 두 파라미터의 값을 바꾸는 Swap함수를 구현하는 예시가 있습니다.

    본문에서는 해당 내용에 대한 기초적인 개념 즉, 볼드체 처리 된 네 용어에 대해 간략히 살펴본 후

    그것이 객체지향적 관점에서 어떤 맹점이 되어 다가오는지

    그리고 그것을 어떻게 다루어야 하는지 살펴 볼 예정입니다.

     


     

    1. Swap함수

     

    더보기

    바로 예제부터 살펴보도록 하겠습니다.

     

    Example (깊은 복사, 값에 의한 호출)

    #include <iostream>
    using namespace std;
    
    void deep_swap(int a, int b){
        cout << "2 " << &a << " : " << a << " " << &b << " : " << b << "\n";
        int temp = a;
        a = b;
        b = temp;
        cout << "3 " << &a << " : " << a << " " << &b << " : " << b << "\n";
    }
    
    int main(){
        int a = 10;
        int b = 20;
        
        cout << "1 " << &a << " : " << a << " " << &b << " : " << b << "\n";
        deep_swap(a, b);
        cout << "4 " << &a << " : " << a << " " << &b << " : " << b << "\n";
    }
    1 AA : 10 BB : 20
    2 DD : 10 EE : 20
    3 DD : 20 EE : 10
    4 AA : 10 BB : 20

    해당 예제는 잘못된 Swap함수의 구현에 대한 예제입니다.

    출력의 주소값 부분은 시스템에서 할당하는 주소값이므로 알아보기 편하게 고쳐서 적어보았습니다.

    깊은 복사 (Shallow copy)는 객체에 대한 복사가 일어날 때 새로운 메모리를 할당해서 복사가 일어나는 것을 의미합니다.

    값에 의한 호출 (Call by value)는 함수의 호출 시 전달되는 파라미터에 깊은 복사를 수행하는 것을 의미합니다.

    위 예제는 Swap함수에서 깊은 복사가 일어나 호출하는 측의 값의 교체가 일어나지 않음을 볼 수 있습니다.

     

    Example (얕은 복사, 참조에 의한 호출)

    #include <iostream>
    using namespace std;
    
    void shallow_swap(int& a, int& b){
        cout << &a << " : " << a << " " << &b << " : " << b << "\n";
        int temp = a;
        a = b;
        b = temp;
        cout << &a << " : " << a << " " << &b << " : " << b << "\n";
    }
    
    int main(){
        int a = 10;
        int b = 20;
        
        cout << &a << " : " << a << " " << &b << " : " << b << "\n";
        shallow_swap(a, b);
        cout << &a << " : " << a << " " << &b << " : " << b << "\n";
    }
    1 AA : 10 BB : 20
    2 AA : 10 BB : 20
    3 AA : 20 BB : 10
    4 AA : 20 BB : 10

    참조자를 이용한 올바르게 구현된 Swap함수입니다.

    얕은 복사 (Shallow copy)는 객체에 대한 복사가 일어날 때 해당 객체에 접근하는 포인터만 하나 늘어나는 것을 의미합니다.

    참조에 의한 호출 (Call by reference)는 함수의 호출 시 전달되는 파라미터에 얕은 복사를 수행하는 것을 의미합니다.

    위 예제는 Swap함수에서 얕은 복사가 일어나 호출 측과 함수 내부에서 가리키는 값의 주소가 같음을 볼 수 있습니다.

     

    2. 복사 생성자

     

    더보기

    복사 생성자 (Copy constructor)는 객체의 복사본을 생성할 때 호출되는 생성자입니다.

    특별한 구분이 있는 만큼, 디폴트 생성자처럼 선언하지 않을 경우 컴파일러가 자동으로 생성합니다.

     

    형태는 다음과 같습니다.

    class MyClass{
        MyClass(const MyClass &rhs);
    };

    파라미터의 이름인 rhs는 Right Hand Side 즉, r-value를 의미합니다.

    const는 생략 가능하지만, 특별한 이유가 없다면 붙이는 것이 원칙입니다.

    복사가 일어났는데 원본이 수정되는 일이 그 특별한 경우라고 볼 수 있겠습니다.

     

    다음은 복사 생성자가 호출되는 예제 입니다.

     

    Example (Copy constructor)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) : my_value(a) {
            cout << "Construct : MyClass\n";
        }
        MyClass(const MyClass& rhs) : my_value(rhs.my_value) {
            cout << "Copy Construct : MyClass\n";
        }
        
        int my_value;
    };
    
    int main(){
        MyClass a(10);
        MyClass b(a);
        
        cout << a.my_value << "\n";
        cout << b.my_value << "\n";
    }
    Construct : MyClass
    Copy Construct : MyClass
    10
    10

    위와 같은 방식으로 복사 생성자를 정의, 호출 할 수 있습니다.

    위의 예제는 인스턴스의 생성에서 복사 생성자가 호출됨을 볼 수 있는데 이와 같은 상황 외에도, 복사 생성자가 호출되는 상황이 몇 가지 존재하고 거기에서 생길 수 있는 몇 가지 문제점이 존재한다.

     

    다음 문단에서 이어집니다.

     

    3. 함수 호출과 복사 생성자

     

    더보기

    복사 생성자는 다음과 같이, 함수 호출에서도 호출 될 수 있습니다.

     

    Example (Copy constructor in function call)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) : my_value(a) {
            cout << "Construct : MyClass\n";
        }
        MyClass(const MyClass& rhs) : my_value(rhs.my_value) {
            cout << "Copy Construct : MyClass\n";
        }
        
        int my_value;
    };
    
    void function(MyClass a){
        a.my_value = a.my_value + 10;
    }
    
    int main(){
        MyClass a(10);
        cout << a.my_value << "\n";
        function(a);
        cout << a.my_value << "\n";
    }
    Constructor : MyClass
    10
    Copy Constructor : MyClass
    10

    다음과 같이 함수의 파라미터가 클래스일 경우

    깊은 복사가 일어나는 상황에서 복사 생성자가 호출되는 것과

    깊은 복사가 일어났기 때문에 원본 인스턴스에 값의 변화가 없음을 볼 수 있습니다.

     

    파라미터로 전달받은 인스턴스의 사본이 필요한 경우가 아니라면, 위와 같은 코드는 비 효율적입니다.

    a라는 인스턴스가 함수로 전달되면서, a의 복사본을 만들기 위한 복사 생성자가 호출되며

    프로그램은 이 복사 생성자가 호출되는 만큼의 오버헤드가 발생하게 됩니다.

    메모리 측면에서도, 복사본을 만들기 위해 추가적인 메모리가 필요합니다.

    이러한 상황의 해결책이 몇가지 존재하는데, 우선 극단적인 예시부터 살펴보도록 하겠습니다.

     

    Example (Delete copy constructor)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) : my_value(a) {
            cout << "Construct : MyClass\n";
        }
        MyClass(const MyClass& rhs) = delete;
        
        int my_value;
    };
    
    void function(MyClass a){
        a.my_value = a.my_value + 10;
    }
    
    int main(){
        MyClass a(10);
        cout << a.my_value << "\n";
        function(a); // Line 23 : Error
        cout << a.my_value << "\n";
    }
    C2280 : 'MyClass::MyClass(const MyClass &)': 삭제된 함수를 참조하려고 합니다.

    극단적으로, 복사 생성자 자체를 삭제해버리는 것 입니다.

    몇가지 불편한 점은 생기겠지만, 복사 생성자를 호출함으로써 발생되는 오버헤드를 일절 차단할 수 있습니다.

     

    다음은 이보다 더 간단한 방법입니다.

     

    Example (Call by reference)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) : my_value(a) {
            cout << "Construct : MyClass\n";
        }
        MyClass(const MyClass& rhs) : my_value(rhs.my_value) {
            cout << "Copy Construct : MyClass\n";
        }
        
        int my_value;
    };
    
    void function(MyClass& a){ // Line 16 : Here!
        a.my_value = a.my_value + 10;
    }
    
    int main(){
        MyClass a(10);
        cout << a.my_value << "\n";
        function(a);
        cout << a.my_value << "\n";
    }
    Construct : MyClass
    10
    20

    변경된 코드에 주석으로 표시를 해 두었습니다.

    함수의 파라미터를 참조자로 변경해서, Call by reference가 되도록 만든 것입니다.

    위와 같이 수정하면 복사 생성자의 호출도 방지할 수 있고, 인스턴스를 온전히 이용할 수 있습니다.

     

    다만, 함수의 호출에 의해 인스턴스가 수정되는 것은 바람직하지 못한 설계입니다.

    파라미터에 클래스가 있을 경우, const를 통해 상수형으로 참조하여 인스턴스의 수정을 방지하도록 하는 것이 좋습니다.

    함수에서 인스턴스를 수정하는 것이 왜 바람직하지 못한 설계인가?

    에 대한 내용은, 차후 다른 글에서 다루어 보도록 하겠습니다.

     

    4. 얕은 복사와 동적 할당

     

    더보기

    3번 문단에서 소개한 복사 생성자의 호출에 의한 시간, 메모리 오버헤드보다 더 심각한 문제가 존재합니다.

    예제를 통해 살펴보도록 하겠습니다.

     

    Example (Dynamic allocation in class)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) {
            my_value = new int;
            *my_value = a;
            cout << "Construct : MyClass\n";
        }
        ~MyClass() {
            delete my_value;
        }
        
        int *my_value;
    };
    
    int main(){
        MyClass a(10);
        MyClass b(a);
        cout << &*a.my_value << " " << &*b.my_value << "\n";
    }
    Construct : MyClass
    AA AA
    //Runtime Error

    바로 클래스에 동적 할당 하는 멤버 변수가 존재할 경우입니다.

    위 클래스는 복사 생성자에 디폴트 복사 생성자가 이용되었고

    그 형태는 다음과 같습니다.

    class MyClass {
    public:
        MyClass(const MyClass& rhs) {
            my_value = rhs.my_value;
        }
    };

    위의 a.my_value와 b.my_value가 가리키는 곳의 주소값이 같아지게 됨을 볼 수 있습니다.

    따라서 복사 생성자에서 깊은 복사를 수행하지 않고, 얕은 복사를 수행했음을 알 수 있으며

    프로그램이 종료되는 시점에 a의 소멸자가 호출되어 a.my_value의 할당이 해제됨과 동시에

    b.my_value가 가리키는 포인터 또한 할당이 해제되어 오류가 발생했음을 유추할 수 있습니다.

     

    위의 문제를 해결하기 위해서 복사 생성자가 깊은 복사를 수행하도록 정의하면 다음과 같습니다.

     

    Example (Deep copy in class)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) {
            my_value = new int;
            *my_value = a;
            cout << "Construct : MyClass\n";
        }
        MyClass(const MyClass& rhs) {
            my_value = new int;
            *my_value = *rhs.my_value;
            cout << "Copy Construct : MyClass\n";
        }
        ~MyClass() {
            delete my_value;
        }
        
        int *my_value;
    };
    
    int main(){
        MyClass a(10);
        MyClass b(a);
        cout << &*a.my_value << " " << &*b.my_value << "\n";
    }
    Construct : MyClass
    Copy Construct : MyClass
    AA BB

    복사 생성자에서 깊은 복사를 수행하도록 수정했습니다.

    인스턴스 간 멤버 변수의 주소값이 서로 달라짐을 볼 수 있습니다.

     

    이와 비슷한 문제가 복사 생성자 외에 추가로 존재합니다.

    예제를 살펴보도록 하겠습니다.

     

    Example (Assignment operator)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) {
            my_value = new int;
            *my_value = a;
            cout << "Construct : MyClass\n";
        }
        ~MyClass() {
            delete my_value;
        }
        
        int *my_value;
    };
    
    int main(){
        MyClass a(10);
        MyClass b(20);
        a = b; // Line 21 : Here!
        cout << &*a.my_value << " " << &*b.my_value << "\n";
    }
    Construct : MyClass
    Construct : MyClass
    AA AA

    주석으로 표시된 a = b의 문법에서 얕은 복사가 수행되었습니다.

    기본 자료형 외에 구조체나 클래스에도 대입 연산자가 적용되는데, 디폴트 대입 연산자얕은 복사를 수행하여 위의 복사 생성자와 동일한 문제가 발생하는 것 입니다.

     

    이 문제를 해결하기 위해, 대입 연산자의 작동 방식을 수정해 줄 필요가 있습니다.

    연산자 오버로딩 (Operator overloading)을 이용하는 것인데, 이것은 클래스에 대한 연산자의 작동 방식을 함수처럼 오버로딩 하는 것 입니다.

    자세한 내용은 [C++] 연산자 에 서술하도록 하겠습니다.

     

    Example (Operator overloading)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) {
            my_value = new int;
            *my_value = a;
            cout << "Construct : MyClass\n";
        }
        ~MyClass() {
            delete my_value;
        }
        MyClass& operator=(const MyClass& rhs) {
            *my_value = *rhs.my_value;
            return *this; // Line 16
        }
        
        int *my_value;
    };
    
    int main(){
        MyClass a(10);
        MyClass b(20);
        a = b;
        cout << &*a.my_value << " " << &*b.my_value << "\n";
    }
    Construct : MyClass
    Construct : MyClass
    AA BB

    연산자의 작동 방식을 얕은 복사에서 깊은 복사로 수정했습니다.

    출력에서 주소값이 동일해지지 않은 것을 확인 할 수 있습니다.

    추가로, this포인터를 반환하는 것은 a = b = c; 와 같은 코드가 정상 작동하게 하기 위함입니다.

     

    5. 변환 생성자

     

    더보기

    변환 생성자란, 매개변수가 한 개인 생성자를 의미합니다.

    변환 생성자는 다른 자료형에 대한 묵시적인 형 변환을 제공합니다.

    예제를 살펴보도록 하겠습니다.

     

    Example (Conversion constructor)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int val) : my_value(val){
            cout << "Construct : MyClass\n";
        }
        
        int my_value;
    };
    
    void func(MyClass arg){
        cout << arg.my_value << "\n";
    }
    
    int main(){
        func(5);
    }
    Construct : MyClass
    5

    MyClass가 int자료형에 대한 변환 생성자를 제공하므로, func(5)는 문법적으로 오류가 없습니다.

    매개변수로 전달된 int가 변환 생성자를 통해 MyClass가 되는 것 입니다.

    그런데 func 함수를 보면 클래스를 매개변수로 받음에도 상수형과 참조자가 선언되지 않았음을 볼 수 있습니다.

    상기 문단에서 언급한 내용과는 맞지 않으니, func 함수를 수정하도록 하겠습니다.

     

    Example (Temporary object)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int val) : my_value(val){
            cout << "Construct : MyClass\n";
        }
        ~MyClass(){
            cout << "Destruct : MyClass\n";
        }
        
        int my_value;
    };
    
    void func(const MyClass& arg){
        cout << arg.my_value << "\n";
    }
    
    int main(){
        func(5);
    }
    Construct : MyClass
    5
    Destruct : MyClass

    상수형과 참조자를 선언했음에도 불구하고, 여전히 묵시적으로 형 변환이 됨을 볼 수 있습니다.

    출력을 보면, 컴파일러가 임시 객체를 생성해서 func함수 내에서 이용했음을 알 수 있습니다.

    이것이 어느정도의 오버헤드를 낳을 수 있는지, 다음의 예제를 살펴보도록 하겠습니다.

     

    Example (Conversion constructor between classes)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int val) : my_value(val){
            cout << "Construct : MyClass\n";
        }
        MyClass(MyClass& rhs) : my_value(rhs.my_value){
            cout << "Copy Construct : MyClass\n";
        }
        ~MyClass(){
            cout << "Destruct : MyClass\n";
        }
        
        int my_value;
    };
    
    class TempClass{
    public:
        TempClass(MyClass val){
            temp_value = 10;
            cout << "Construct : TempClass\n";
        }
        ~TempClass(){
            cout << "Destruct : TempClass\n";
        }
        
        int temp_value;
    };
    
    void func(const TempClass& arg){
        cout << arg.temp_value << "\n";
    }
    
    int main(){
        MyClass a(5);
        func(a);
    }
    Construct : MyClass
    Copy Construct : MyClass
    Construct : TempClass
    Destruct : MyClass
    10
    Destruct : TempClass
    Destruct : MyClass

    변환 생성자는 기본 자료형 외에 클래스 사이에도 적용이 됩니다.

    MyClass에 대한 변환 생성자를 제공하는 TempClass를 추가로 만들어 TempClass를 매개변수로 받는 func함수에 MyClass를 인자로 전달한 결과입니다.

    순서대로 다음과 같은 절차가 진행되었습니다.

    1. MyClass a가 생성되었다.
    2. func에 a가 전달되었다. TempClass는 MyClass에 대한 변환 생성을 제공하므로 문법적 오류가 없다.
    3. a를 TempClass로 변환하기 위한 a의 사본을 만들기 위해 복사 생성자가 호출되었다.
    4. a의 사본으로 TempClass의 변환 생성자가 호출되었다.
    5. TempClass가 정상적으로 생성되었으므로, 사용처가 사라진 a의 사본이 소멸되었다.
    6. 함수가 실행되고, 함수의 마지막에 TempClass가 소멸되었다.
    7. 프로그램의 끝에 MyClass a 가 소멸되었다.

    만약 클래스와 함수를 정의한 사람과, main함수에서 이것을 사용하는 사용자가 다르다고 가정한다면

    위와 같은 오버헤드를 인지하지 못 할 가능성이 매우 크며, 이것은 매우 비 효율적인 구성이라고 볼 수 있습니다.

    만약 사용자의 편의성을 높여주는 것이 목적이라면 묵시적 변환을 허용할 수 있지만 프로그램의 효율성을 고려한다면, 이러한 변환을 막는 것이 좋습니다.

    아래와 같은 방식으로, 묵시적 형 변환을 원천적으로 차단 할 수 있습니다.

     

    Example (Explicit conversion constructor)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        explicit MyClass(int val) : my_value(val){
            cout << "Construct : MyClass\n";
        }
        ~MyClass(){
            cout << "Destruct : MyClass\n";
        }
        
        int my_value;
    };
    
    void func(const MyClass& arg){
        cout << arg.my_value << "\n";
    }
    
    int main(){
        func(5);
    }
    C2664 : 'void func(const MyClass &)': 인수 1을(를) 'int'에서 'const MyClass &'(으)로 변환할 수 없습니다.

    explicit 예약어를 사용하는 것으로 묵시적인 변환을 막을 수 있습니다.

    다음과 같은 상황에서 func함수에 5의 값을 전달해야 할 경우

    func(MyClass(5))와 같이 명시적으로 임시 객체를 생성해야 합니다.

     

    6. 형 변환 연산자

     

    더보기

    이전 문단에서 변환 생성자를 통해 int와 MyClass사이의 호환성이 생긴 것을 볼 수 있었습니다.

    하지만 MyClass가 int로 변환될 수 없는 구조이므로, 이것은 완벽한 호환성이라고 볼 수 없습니다.

    MyClass에 형 변환 연산자를 정의하는 것으로 int와 MyClass 사이의 호환성을 만들 수 있습니다.

     

    예제를 살펴보도록 하겠습니다.

    Example (Conversion operator)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        explicit MyClass(int val) : my_value(val){
            cout << "Construct : MyClass\n";
        }
        ~MyClass(){
            cout << "Destruct : MyClass\n";
        }
        operator int(void) { return my_value; }
        
        int my_value;
    };
    
    int main(){
        MyClass a(5);
        cout << a << "\n"; // Line 19
        cout << a.my_value << "\n";
        cout << (int)a << "\n";
        cout << static_cast<int>(a) << "\n"; // Line 22
    }
    Constrcut : MyClass
    5
    5
    5
    5
    Destruct : MyClass

    19번째 줄의 코드를 살펴보겠습니다.

    MyClass에 int자료형으로의 형 변환 연산자가 정의된 것으로, 해당 코드가 묵시적으로 int로 형 변환 된 것입니다.

    22번째 줄의 코드에 있는 static_cast<int>()는 (int)와 같은 강제 형 변환이 아닌, C++의 형 변환 연산자입니다.

    static_cast는 형 변환이 허용되는 것으로 변환을 시도한다는 차이점이 있습니다.

     

    이와 같이 C++에서 추가된 형 변환 연산자는 다음과 같습니다.

    const_cast<>
    static_cast<>
    dynamic_cast<>
    reinterpret_cast<>

    각각의 연산자의 역할에 [C++] 형 변환 연산자 에 서술하도록 하겠습니다.

    또한, 형 변환 연산자에도 explicit 예약어가 적용이 됩니다.

    위의 소스 코드에서 int 형 변환 연산자에 explicit 예약어를 추가할 경우 19번째 줄의 묵시적 변환이 허용되지 않으므로, 컴파일 에러가 발생합니다.

     

    묵시적 변환은 사용자에게 있어서는 편하지만

    개발자의 입장에서는 사용자가 일으킬 수 있는 오류가 늘어남을 의미합니다.

    편의성과 안정성을 잘 조율할 수 있도록 주의합시다.

     

    7. 이름 없는 임시 객체

     

    더보기

    위 문단에서 우리는 여러 상황에서 임시 객체가 생성되는 것을 볼 수 있었습니다.

    복사 생성자를 통해, 변환 생성자를 통해 생성되는 임시 객체들은 몇 가지만 주의하면 생성을 막을 수 있었습니다.

    하지만 이 객체는 조금 다릅니다.

     

    예제를 살펴보도록 하겠습니다.

     

    Example (Temporary object)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(int a) : my_value(a){
            cout << "Construct : MyClass " << my_value << "\n";
        }
        MyClass(MyClass& rhs) : my_value(rhs.my_value){
            cout << "Copy Construct : MyClass " << my_value << "\n";
        }
        ~MyClass(){
            cout << "Destruct : MyClass " << my_value << "\n";
        }
        MyClass& operator=(const MyClass& rhs){
            cout << "Operator = \n";
            my_value = rhs.my_value;
            return *this;
        }
        
        int my_value;
    };
    
    MyClass func(int val){
        MyClass temp(val);
        return temp;
    }
    
    int main(){
        MyClass a(10);
        a = func(5); // Line 31
        cout << a.my_value << "\n";
    }
    Construct : MyClass 10
    Construct : MyClass 5
    Copy Construct : MyClass 5
    Destruct : MyClass 5
    Operator =
    Destruct : MyClass 5
    5
    Destruct : MyClass 5

    이번에 살펴 볼 임시 객체의 특징은, 이름이 없다는 것 입니다.

    함수의 반환 형식이 클래스일 경우 생성되는 객체이며, 반환을 위한 객체의 사본을 반환하는 형식 입니다.

    대입을 위해 사본을 이용하는 것은 그럭저럭 납득할 수 있는 상황입니다.

    하지만 Line 31을 func(5); 로 수정하여도 동일하게 복사 생성자가 호출됩니다.

    함수 호출 시 복사 생성자로 사본이 반환된 후, 곧바로 소멸하는 것 입니다.

     

    객체의 생명주기에 대해 확실하게 파악하는 것은 매우 중요합니다.

    아무런 소득이 없는 불필요한 소스코드를 작성하지 않도록 주의합시다.

     

    8. r-value 참조

     

    더보기

    r-value에 대한 참조자는 C++11 표준에서 새로 제공된 참조자 입니다.

    기존 참조자가 &이고, r-value 참조자는 &&입니다.

    r-value가 연산의 결과에 따라 생성된 임시 객체라는 것을 떠올리며 아래의 예제를 살펴보도록 하겠습니다.

     

    Example (r-value reference)

    #include <iostream>
    using namespace std;
    
    void func(int& param){
        cout << "Function &int\n";
    }
    
    int main(){
        func(1 + 2);
    }
    C2664 : 'void func(int &)': 인수 1을(를) 'int'에서 'int &'(으)로 변환할 수 없습니다.

    위 func함수의 매개변수를 int&&로 바꾸면 위 소스는 작동합니다.

    r-value참조는 위와 같은 상황을 위해 존재합니다.

    조금 더 자세한 예시를 표를 통해 살펴보도록 하겠습니다.

     

    매개변수 형식 실인수 예시 비고
    func(int) int x = 1;
    func(x);
    func(1);
    func(1 + 2);
     
    func(int &) int x = 1;
    func(x);
     
    func(int &&) int x = 1;
    func(1);
    func(1 + 2);
    func(x + 1);
    func(x)는 불가능하다.

    위와 같은 상황들을 고려해 함수의 매개변수의 참조형을 잘 결정해야 합니다.

    물론 오버로딩을 할 수 있지만, 모호성이 발생할 수 있음에 유의합시다.

     

    Example (Ambiguous function)

    #include <iostream>
    using namespace std;
    
    void func(int param){
        cout << "Function int\n";
    }
    
    void func(int&& param){
        cout << "Function &&int\n";
    }
    
    int main(){
        func(1 + 2);
    }
    C2668 : 'func': 오버로드된 함수에 대한 호출이 모호합니다.

    r-value에 대한 참조는 위의 예제와 같은 기본 자료형이 아닌, 클래스에 적용될 때 필요하게 됩니다.

    다음 문단인 '이동 시맨틱'을 살펴보겠습니다.

     

    9. 이동 시맨틱

     

    더보기

    이동 시맨틱은 이동 생성자와 대입 연산자로 구현됩니다.

    이전 문단들에서 살펴보았듯이, 임시 객체가 생성되는 것은 프로그램에 적지 않은 오버헤드를 발생시킵니다.

    이것을 최소화 시키기 위한 방법 중 하나가 이동 시맨틱입니다.

    미리 요약하자면 다음과 같습니다.

    복사 생성자와 대입 연산자에 r-value참조를 조합하여 새로운 생성 및 대입의 경우를 만든 것.

    예제를 살펴보자.

     

    Example (Move semantics)

    #include <iostream>
    using namespace std;
    
    class MyClass{
    public:
        MyClass(){
            cout << "Construct : MyClass " << my_value << "\n";
        }
        MyClass(MyClass& rhs) : my_value(rhs.my_value){
            cout << "Copy Constrcut : MyClass " << my_value << "\n";
        }
        MyClass(MyClass&& rhs) : my_value(rhs.my_value){
            cout << "Move Constrcut : MyClass " << my_value << "\n";
        }
        ~MyClass(){
            cout << "Destruct : MyClass " << my_value << "\n";
        }
        
        MyClass& opeartor=(const MyClass& rhs) = default;
        
        void set_value(int val){ my_value = val; }
        
        int my_value = 0;
    };
    
    MyClass func(int val){
        cout << "Function Start\n";
        MyClass temp;
        temp.set_value(val);
        cout << "Function End\n";
        return temp;
    }
    
    int main(){
        MyClass a;
        cout << "Main Start\n";
        a = func(10);
        MyClass b(a);
        cout << "Main End\n";
    }
    Construct : MyClass 0
    Main Start
    Function Start
    Construct : MyClass 0
    Function End
    Move Construct : MyClass 10
    Destruct : MyClass 10
    Destruct : MyClass 10
    Copy Construct : MyClass 10
    Main End
    Destruct : MyClass 10
    Destruct : MyClass 10

    7문단 이름 없는 임시 객체의 예제 코드에서 몇가지 수정사항만 있는 소스코드 입니다.

    출력의 차이점은 복사 생성자가 이동 생성자로 대체되었다는 것 외에는 크게 다르지 않습니다.

    이것이 무엇을 의미하는지는, 메모리의 관점에서 바라보도록 하겠습니다.

    이전 예제에서의 복사 생성은 전달하고자 하는 객체의 사본을 생성하는 것 (깊은 복사) 이었지만

    이번 예제의 이동 생성은 전달하고자 하는 객체의 참조자를 전달합니다 (얕은 복사)

    어차피 대입 후 소멸될 객체들이기 때문에, 얕은 복사를 통해 성능을 향상시키는 아이디어 입니다.

     

    기존 문단에서 주로 얕은 복사가 만들어 내는 문제점에 대해서 살펴보았는데

    이번에는 오히려 얕은 복사가 해답이 되었음을 볼 수 있습니다.

     


     

    이전까지의 글보다 볼륨이 조금 커진 것 같습니다.

    쓰면서도 이걸 두 파트로 나눠서 써야했나... 싶기도 합니다.

    이번 주제는 주로 메모리와 관련된 내용이었는데

    최근 기기들의 기본적인 스펙이 많이 상승한 탓도 있고

    관련된 분야 (임베디드, 게임 등)의 현장을 경험해보지 못한 탓도 있어서

    굉장히 안 와닿는 내용이었습니다.... 만

    모르면 바보되기 딱 좋을거같긴 합니다.

     

    다음 글은 연산자 입니다.

    긴 글 읽어주셔서 감사합니다.

    'C++ > 이것이 C++이다' 카테고리의 다른 글

    [C++] 상속 기본  (0) 2021.08.13
    [C++] 연산자  (0) 2021.07.16
    [C++] 클래스 기본 문법 2  (0) 2021.03.12
    [C++] 클래스 기본 문법 1  (0) 2021.02.27
    [C++] 클래스를 살펴보기 전에  (0) 2021.02.27

    댓글

Designed by Tistory.