Eat Study Love

먹고 공부하고 사랑하라

SW 만학도/C++

7. Copy&Move, Special Members

eatplaylove 2024. 4. 12. 00:03

https://eglife.tistory.com/50

 

6. Out_of_class Definition & Operator Overloading

https://eglife.tistory.com/49 5. Classes https://eglife.tistory.com/43 4. Functions and Memory Management https://eglife.tistory.com/36 3. C++ Standard Library (3) https://eglife.tistory.com/35 2. C++ Standard Library (2) https://eglife.tistory.com/34 1. C

eglife.tistory.com

 

 

깨달은 게 있다.

 

뭔가 뇌가 꼬이는 거 같고, 이해가 너무 안 될 때는 일단 생각하기를 멈추고 바람좀 쐬고 와야한다.

 

그렇게 이해가 안 가서 징징거리던 ch6 Out of class definition과 Operator Overload 부분이,, 나중에 보니까 그래도 좀 이해가 슬슬 되기 시작했다.

 

그리고 세상에 감사하게도 Chat GPT를 쓰니까 넘 든든했다.

 

어디 뭐 물어볼 곳도 딱히 없었는데 웬만한 교수님급 지식을 갖고 있는 녀석이  함께해주니 이렇게 좋을 수 없다^^

 

각설하고, 공부는 지속된다..

 

Overview --> Special members ( Copy and move semantics & Static members ) , Debugging

 

 

Copy Semantics

 

#include <iostream>
using namespace std;

class SimpleVector{
public:
    int* array; //points to elements
    int size; // # of elements
    int capacity; // capa of array

    SimpleVector(int initialCapacity):
        size(0),capacity(initialCapacity){
            array = new int[capacity]; // array dynamic 할당
        }

        ~SimpleVector(){
            delete[] array;
        }
    void addElement(int element){
        if(size == capacity)
            resize();
            array[size] = element;
            size++;
    }
    void resize(){
        capacity = capacity * 2;
        int* newArray = new int[capacity];
        for(int i=0;i<size;i++)
            newArray[i] = array[i];
        delete[] array;
        array = newArray;

    }

    int getSize(){
        return size;
    }
};

Error!!!!!!!!

- 처음에 vec1에 new로 dynamic memory를 할당한 뒤 size 2, capa2의 0x6080 주소를 갖는 array를 만들고 {1,2}로 초기화 한다.

 

- 그리고 vec2에 vec1를 덮어 씌운다.

 

(위 코드참고해서 함수동작 이해필요)

 

- vec1에 addElement(3)을 하면서 size 늘리고(size == capa라 resize필요) capa는 2배로는다. 이 과정에서 기존에 있던 memory를 delete하고 capa가 커진 memory로 이사를 간다.

 

- 그리고 vec2에 addElement(4)를 할 때 error 발생.. 역시 resize를 하려고 하니, 이미 해당 memory는 free가 되었는데 왜 또 delete를 하려고 하냐는 Error가 뜬다. ----> Deep Copy가 必

 

 

기존의 Copy case

void processVector도 myVec을 copy하는 것이라고 생각허자

 

 

- 이렇게 일시적인 copy는 error를 초래하기 때문에 non temporary object를 통해 deep copy를 진행하는 것이 필요하다.

 

Copy Construct

template <typename T>
SimpleVector<T>::SimleVector(const SimpleVector<T>& other):
    size(0),capacity(other.capacity),array(new T[other.capacity()]){
                //copy the elements
                for(int i=0;i<other.size;++i){
                    addElement(other.array[i]);
                }
    }

 

- const reference를 받는 constructor를 만들어 준다. for문에서 element copy가 발생함.

 

- 새롭게 memory공간할당을 하고, copy하려는 놈을 one by one으로 옮겨준다.

 

Copy Assignment Operator

- 이미 initialize 되어 있는 object를 다른 non-temporary object로 DEEP COPY 하는 operator를 생각해보자

 

template <typename T>
SimpleVector<T>& SimpleVector<T>::operator=(const SimpleVector<T>& other){
            if(this != &other) //this는 현재 array의 주소(T* array)이다.
            {delete[] array;
            capacity = other.capacity;
            array = new T[capacity];
            size = 0;
            for(int i=0;i<other.size;i++){
                addElement(other.array[i]);
                }
            }
            return *this;
}

 

- 기본적으로 지금 하고자 하는 건, 현재 SimpleVector의 객체에 other라는 SimpeVector<T>&의 내용을 copy하려는 것이다.

 

- 빼껴먹으려는 other의 내용이 변하면 안 되기에 input단에 const를 넣는다. 그리고, 누누히 다뤘듯이, other라는 객체의 메모리 복제없이 내용만을 참조하기 위해서 <T>& 꼴로 Reference를 부른 것이다.

 

- this는 무엇이냐? this는 이제 copy할 내용이 들어갈 새로운 SimpleVector class의 객체를 포인팅하는 variable이다.

 

- 엥? this가 뭔데 pointer냐

 

this 포인터가 SimpleVector 인스턴스의 포인터라는 것은 C++ 언어의 규칙과 클래스 멤버 함수의 작동 방식에 기반합니다. 클래스의 멤버 함수 내에서 this는 자동으로 현재 인스턴스(즉, 메서드가 호출된 객체)를 가리키는 포인터로 제공됩니다. 이는 C++ 언어의 일부이며, 컴파일러에 의해 자동으로 처리됩니다.
SimpleVector<T>::operator=와 같은 멤버 함수에서 this는 다음과 같은 특징을 가집니다:
  1. 현재 인스턴스의 포인터: this는 멤버 함수가 호출된 SimpleVector<T> 인스턴스의 포인터입니다. 함수가 어떤 객체에 대해 호출되면, this는 그 객체의 주소를 가리킵니다.
  2. 멤버 접근: this 포인터는 현재 인스턴스의 멤버에 접근하는 데 사용됩니다. 예를 들어, this->array, this->size, this->capacity 등은 현재 SimpleVector<T> 객체의 array, size, capacity 멤버를 가리킵니다. 하지만, 일반적인 경우 this->는 생략되며, 단순히 array, size, capacity로 작성해도 동일한 객체의 멤버를 가리킵니다.
  3. 자동 포인터: 멤버 함수 내에서 this는 자동으로 사용할 수 있습니다. 개발자가 명시적으로 선언하거나 할당할 필요가 없으며, 함수가 객체의 컨텍스트에서 실행될 때 컴파일러에 의해 자동으로 제공됩니다.
결론적으로, this의 사용과 그 의미는 C++의 표준에 의해 정의되며, 모든 클래스 멤버 함수 내에서 자동으로 사용할 수 있는 내장 포인터입니다. 클래스의 인스턴스 메서드 내에서 this는 항상 해당 인스턴스를 가리키는 것으로 간주됩니다.

 

- 답변은 위와 같다고 한다. by GPT. C++의 약속이라고 한다. Class의 Member Method 안에서 this는 자동으로 현재 Class의 instance를 가르키는 포인터로 제공된다고 한다. 이 때문에 this가 처음보는 녀석이라고 생각이 들어도 Class 내부에서 정의했던 array, size, capacity 요런 것에 접근이 가능한 것이다.

 

- 참고로 우리가 지금 vec1 = vec2 를 써서 copy를 진행하고 싶다면 여기서 this는 vec1을 가르키는 pointer이고, other는 vec2를 의미한다.(정확히 말하면 vec2를 Reference/참조 한 것)

 

            for(int i=0;i<other.size;i++){
                addElement(other.array[i]);
                }
            }

 

 

- 이건 for문에 대한 개인적인 궁금증인데, 왜 other[i]를 쓰면 안 되는 지 궁금했다.

A1 . other.array[i]other[i]로 바꾸는 것이 가능한지 여부는 SimpleVector<T> 클래스의 구현에 달려 있습니다. 만약 SimpleVector<T> 클래스에 operator[]가 정의되어 있어T& operator[](int index) 형태로 외부에서 접근 가능하다면, other[i]로 사용하는 것이 가능합니다.

+Q2 : array의 경우 class public에 T* array;인 포인터로 정의 되어 있는데 그냥 other.array[i]로 other에 있는 array값에 접근이 가능해? dereference해줘야 하는 거 아니야?


 A2 : other.array[i]를 사용하는 경우, array는 T* 타입의 포인터로, T 타입의 동적 배열을 가리킵니다. other.array[i] 표현식에서, other.array는 배열을 가리키는 포인터이며, other.array[i]는 이 배열의 i번째 요소에 접근하는 것입니다. 여기서 i번째 요소에 접근하는 것은 배열의 포인터에 대한 인덱싱(indexing) 연산을 수행하는 것이며, 이는 자동으로 해당 요소를 참조합니다 (즉, 해당 요소를 디레퍼런싱(dereferencing)하는 것과 동일합니다).
따라서, other.array[i]는 *(other.array + i)와 동일한 동작을 합니다. 여기서 other.array + i는 배열의 시작부터 i 요소만큼 떨어진 위치를 가리키는 포인터이며, * 연산자는 이 포인터가 가리키는 위치의 값을 가져옵니다.
하지만 C++에서 배열을 사용할 때는 일반적으로 array[i] 표기법을 사용하여 간단하고 읽기 쉽게 만듭니다. 이는 내부적으로 포인터 연산과 디레퍼런싱을 처리하지만, 개발자가 명시적으로 이러한 세부 사항을 작성할 필요는 없습니다.
만약 array가 private 또는 protected 멤버라면, 클래스 외부에서 직접 접근할 수 없습니다. 이 경우, 클래스 내에서 public으로 정의된 메서드나 friend 함수를 통해서만 접근이 가능합니다. 하지만 public으로 선언된 경우에는 클래스 외부에서도 직접 접근할 수 있습니다.

 

- Pointer에 대한 기본적인 이해가 부족했다. int * ptr = new int[3] { 1, 2 , 3} 이렇게 array를 가르키고 있는 포인터 ptr이 있다고 하면, ptr[1] -> 2 가 출력되고 이는 *(ptr+1) 과 같다는 것이다. array를 포인팅하고 있을 때 대괄호를 쓰는 것 자체가 이미 dereference를 하겠다는 뜻인 것!

 

- Operator의 input과 output type에 관련한 추가 궁금사항

SimpleVector<T> SimpleVector<T>::operator=return this;를 사용하는 것은 문법적으로 올바르지 않으며, 메서드 체이닝을 사용할 수 없습니다. 여기에는 몇 가지 중요한 이유가 있습니다:

  1. 반환 타입 불일치: return this;는 SimpleVector<T>* 타입 (포인터)을 반환합니다. 하지만 SimpleVector<T> SimpleVector<T>::operator=의 선언에 따르면, 반환 타입은 SimpleVector<T> (객체)입니다. 따라서 this 포인터를 반환하는 것은 반환 타입과 일치하지 않습니다.
  2. 메서드 체이닝: operator=의 표준 구현은 객체의 참조(SimpleVector<T>&)를 반환합니다. 이렇게 하면 여러 대입 연산을 체이닝할 수 있습니다 (예: vec1 = vec2 = vec3;). 객체의 포인터를 반환하면, 이런 종류의 체이닝이 불가능합니다.
따라서, operator=를 정의할 때는 일반적으로 객체의 참조를 반환하는 것이 좋습니다. 즉, SimpleVector<T>& SimpleVector<T>::operator= 형태로 선언하고 return *this;를 사용하는 것이 올바른 방법입니다. 이렇게 하면 메모리와 성능 효율이 좋고, C++의 일반적인 관행에 부합하며, 메서드 체이닝이 가능합니다.

 

- Output은 Reference을 반환해야 그거 가지고 Method Chaining을 할 수 있고, this는 기본적으로 포인터이다.

 

- int *ptr = &a 처럼 정의하듯이, 기본적으로 참조(Reference) 타입과 *this를 같이 쓰는 것이 맞다.

 

- 흠.. this는 SimpleVector<T> vec = {1,2,3} 객체를 가르키는 포인터이면 *this는 1을 가르키게 된다. 근데 그 1을 그 값 자체로 반환하려면 SimpleVector<T> SimpleVector<T>::operator~ 이렇게 해도 되는데, 그렇게 하면 그 1을 받는 객체를 추가로 하나 더 복제해서 Method의 Output으로 내는 것이다.

 

 - 그러면 Memory 소요가 되니까, 그 1값의 reference를 반환해준다는 것 --> SimpleVector<T>& SimpleVector<T>::operator~ 

 

 - 실컷 객체(this)에 other에 있던 element 다 복사해서 *this를 만들었는데, 그걸 그대로 반환하지 않고 또 Memory를 써서 거기다가 옮겨적은다음 반환하는 꼴

 

 

Move Semantics

 

- 지금껏 살펴본 Deep copy는 사실 활용하기엔 비싼 녀석이다. 데이터를 옮기고 필요 없어진 녀석은 그냥 메모리 삭제해주는 게 괜찮지 않나..? 라는 관점에서 나오게 된 Move Semantics! 즉, transfer 하는 기능이다.

 

 

 

- 처음에 temporary object 하나를 initialize 해준다.

 

- createVectorMove function은 tempoary object를 받거나[or] local object를 by value로 return 한다... 이게 무슨 소리지???

 

 

 

- 아.. 위 같은 Case에 Move를 사용한다는 뜻

 

template <typename T>
SimpleVector<T>::SimpleVector(SimpleVector<T>&& other) noexcept
    : array(other.array), size(other.size),capacity(other.capacity){

        other.array = nullptr;
        other.size = 0;
        other.capacity = 0;
        // Source 초기화 시키는 과정
    }

 

 

- &&는 rvalue reference를 뜻한다.

 

<T>&&는 C++에서 r-value reference를 나타냅니다. 이는 C++11 표준에서 도입된 개념으로, 오른쪽 값(r-value)에 대한 참조를 의미합니다. r-value reference는 주로 임시 객체나 이동될 수 있는 객체를 참조하는 데 사용됩니다.

R-Value Reference란?

  • 오른쪽 값(R-Value): 오른쪽 값은 표현식이 끝난 후에 지속되지 않는 값입니다. 예를 들어, 임시 값이나 리터럴 값과 같은 것들이 여기에 해당합니다.
  • R-Value Reference (&&): 이는 오른쪽 값에 대한 참조를 나타내며, 주로 이동 의미론(move semantics)과 관련된 연산에 사용됩니다.

이동 생성자와 R-Value Reference

제시된 SimpleVector<T>::SimpleVector(SimpleVector<T>&& other)는 이동 생성자의 예입니다. 이동 생성자는 다른 객체의 리소스를 현재 객체로 '이동'하는 데 사용됩니다. 이동 생성자는 다음과 같은 특징을 가집니다:

  • 리소스 이동: other 객체의 리소스(예: 동적 할당된 메모리)를 현재 객체로 이동시킵니다. 이 경우 array, size, capacity를 현재 객체로 복사하고, other 객체는 무효화합니다.
  • 원본 초기화: other 객체의 포인터를 nullptr로 설정하고, 크기와 용량을 0으로 설정합니다. 이렇게 하면 other가 더 이상 이전 리소스를 참조하지 않도록 합니다.
이동 생성자는 복사 생성자에 비해 효율적입니다. 복사 생성자는 데이터의 깊은 복사(deep copy)를 수행하지만, 이동 생성자는 데이터를 '이동'하기만 하므로 리소스의 복사를 수행할 필요가 없습니다. 따라서, 이는 특히 대용량 데이터를 다룰 때 성능상의 이점을 제공합니다.

 

R-Value Reference (<T>&&)

  • R-Value: R-value는 일반적으로 임시 객체나 리터럴과 같이 이름을 가지지 않고, 식별될 수 없으며, 값을 대입할 수 없는 표현식입니다. 예를 들어, 2 + 2, std::string("hello")와 같은 표현이 r-value에 해당합니다.
  • R-Value Reference (<T>&&): T 타입의 r-value에 대한 참조입니다. 이는 임시 객체를 효율적으로 처리하고, 객체의 리소스를 '이동'하는 데 사용됩니다.

L-Value Reference (<T>&)

  • L-Value: L-value는 메모리상의 고정된 위치를 가진 객체를 의미합니다. 변수나 참조처럼 이름이 있는 개체가 l-value의 예입니다.
  • L-Value Reference (<T>&): T 타입의 l-value에 대한 참조입니다. 이는 기존 객체에 대한 참조를 만들고, 기존 데이터를 변경하거나 사용할 때 사용됩니다.

<T>&&와 <T>&의 차이점

  • 사용 목적: <T>&&는 이동 의미론을 위해 사용됩니다. 즉, 객체의 데이터를 효율적으로 다른 객체로 이동시킬 때 사용됩니다. 이는 리소스 복사 대신 '소유권'을 이전하는 방식입니다. <T>&는 기존 객체에 대한 참조를 만들고, 그 객체를 수정하거나 사용할 때 사용됩니다.
  • 효율성: <T>&&를 사용한 이동 생성자나 이동 대입 연산자는 객체의 데이터를 복사하는 것이 아니라 이동시키므로, 리소스를 복사하는 일반적인 복사 연산보다 더 효율적입니다.
  • 호환성: <T>&&는 r-value (예: 임시 객체)와 함께 사용할 수 있지만, <T>&는 l-value (예: 기존 변수)와 함께 사용할 수 있습니다.

 

- <T> && Rvalue -> 객체이동  , <T>& Lvalue -> 객체 참조 --> 이동X 복사O

 

- noexcept specifier는 말 그대로 constructor가 exception을 던지지 않는다는 것인데.. 걍 Compiler가 필요로하는 기능 정도로 알아두면 될 거 같다.

 

- Transfer를 통해 기종 source object는 접근가능하게 두되, unspecified state로 둔다.

(딱히 가이드가 있는 건 아니다)

 

template <typename T> // move assignment operator
SimpleVector<T>& SimpleVector<T>::operator=(SimpleVector<T>&& other) noexcept{

    if(this != &other) // this 할당 받을 것, other 내용을 this로 transfer하고 other는 free시킬 것
    { delete array;
    array = other.array;
    size = other.size; // 사실 이 모든 것은 this->size 요런식인데 this 생략한 것
    capacity = other.capacity;
    
    other.array = nullptr;
    other.size = 0;
    other.capacity = 0;
    }
    return *this
}

 

 

- array/size/capacity 를 other에서 따 오는 것까진  -->  Swallow Copy

 

- 그 이후에 기존 Source(other)를 초기화 해주는 것  -->  Deep Copy

 

 

Compiler Optimization

 

 

- Compiler에 따라 다르지만 Compiler는 보통 Return Value Optimization(RVO)라는 동작을 지원한다.

 

- Output을 보면 &vec이 function decalre 안과 Main 함수 안에서 불렸을 때 각각 같은 값을 갖는다.

 

- 이는, Compiler는 어차피 함수가 Main에서 쓰이면 그냥 Memory 주소를 Main에다가 만든다는 뜻이다.

 

 

Copy and Move

 

 - Copy Semantics

 - Deep copying the resources of one object ( often non-temporary ) to another

SimpleVector<int> vec{1,2,3};
SimpleVector<int> copiedVec = vec;

 

- Move Semantics

- Transferring the resources of one object ( often temporary ) to another

SimpleVector<int> vec{1,2,3};
vec = SimpleVector<int> {4,5,6};

 

- 상황에 맞게 적절히 이것들을 사용해서 memory leak 등을 막아보즈아..!

 

The Five Rules

 

- Memory를 다룬다면.. 아래 5가지 special member function은 재정의 해줄 것을 권. 고. 함 !

 

 1) Destructor   2) Copy Constructor   3) Copy Assignment Operator

 

 4) Move Constructor   5) Move Assignment Operator

 

 

 

Static Members

특정 Value가 몇 번이나 나왔는지 Count 하고 싶은 Case를 가정해보자

 

 

- Static Member는 Class에 종속되어 있다. 다른 Instance들한텐 종속 ㄴㄴ

 

 - 그치만 class의 모든 instance들에게 share가 된다.

 

 - Class의 instance를 만들지 않아도 access가 가능하다.

 

 - 그럼 이걸 언제 쓰냐? : Shared data/methods, class-wide information, utility functions.

 

template <typename T>
class SimpleVector{
    // class 구문 안에서 선언
    static std::map<T,int> elementcounts;
};

template <typename T>
std::map<T,int> SimpleVector<T>::elementCount = {}; //class 구문 밖에서 초기화

template <typename T>
void SimpleVector<T>::addElement(T element){
    if (size == capacity)
        resize();
    array[size] = element;
    size++;
    elementCount[element]; // element 하나씩 집어넣을때마다 +1 -> element가 몇 번씩 나왔는지 count 가능
}

 

 

- static member에 대한 접근권한은, 이 놈들이 pulic, private 등 어떤 specifier에서 선언되었는 지에 따라 다르다.

 

 

 

- 요렇게만 해줘도 그냥 printElementCount를 call만 하면 vec1, vec2 값들의 구성요소 count가 반환이 되는구나..

 

//static 사용 예상시나리오(for 위 case)

template <typename T>
class SimpleVector {
private:
    // ...
public:
    static std::map<T, int> elementCount; // 각 원소의 카운트를 저장하는 static 맵
    // ...
    static void printElementCount();
};

// elementCount를 정의합니다.
template <typename T>
std::map<T, int> SimpleVector<T>::elementCount = {};

// 생성자에서 원소들을 elementCount에 추가하는 코드를 구현해야 합니다.
template <typename T>
SimpleVector<T>::SimpleVector(std::initializer_list<T> elements) {
    for (const T& element : elements) {
        ++elementCount[element];
    }
    // ...
}

template <typename T>
void SimpleVector<T>::printElementCount() {
    for (const auto& pair : elementCount) {
        cout << pair.first << ":" << pair.second << endl;
    }
}

 

SimpleVector<T>::SimpleVector(std::initializer_list<T> elements) 생성자에서 elementCount에 원소들이 어떻게 추가되는지 구체적으로 설명해드리겠습니다.
먼저 elementCount는 SimpleVector<T> 클래스의 static 멤버 변수입니다. 이는 클래스의 모든 인스턴스 간에 공유되며, 특정 타입 T의 각 원소가 SimpleVector 인스턴스들에 몇 번 나타나는지를 카운트합니다. 이 변수는 일반적으로 std::map<T, int> 타입으로 선언됩니다. 여기서 키(T)는 원소의 값, 값(int)은 그 원소의 출현 횟수입니다.

elementCount에 원소 추가

생성자에서는 std::initializer_list<T> elements를 통해 초기화할 원소들을 받습니다. 이 리스트를 순회하면서 각 원소에 대해 다음과 같이 처리합니다:
  1. 원소 카운트 증가: for (const T& element : elements) 루프는 제공된 모든 원소를 순회합니다. 루프 내에서, ++elementCount[element];는 현재 순회 중인 원소 element의 카운트를 elementCount 맵에서 1만큼 증가시킵니다.
    • 만약 element가 처음 나타나는 원소라면, elementCount 맵에 새로운 항목이 생성되고, 그 카운트가 1로 설정됩니다.
    • 만약 element가 이미 elementCount에 존재한다면, 그 카운트가 1 증가합니다.

예시

SimpleVector<int> vec1{1, 2, 3};

이 코드를 실행할 때 생성자는 다음을 수행합니다:
  • 1, 2, 3 각각에 대해 elementCount 맵의 해당 카운트를 1씩 증가시킵니다. 결과적으로 elementCount[1], elementCount[2], elementCount[3] 각각이 1이 됩니다.
SimpleVector<int> vec2{2, 3, 4};
이 경우에는:

  • 2, 3은 이미 elementCount에 존재하므로 각각의 카운트가 2가 됩니다 (elementCount[2] = 2, elementCount[3] = 2).
  • 4는 새로운 원소이므로 elementCount[4] = 1이 됩니다.
이러한 방식으로 elementCount 맵은 모든 SimpleVector<int> 인스턴스에 걸쳐서 각 정수가 몇 번 나타나는지를 계속 추적합니다.

 

 

끄읕,,,!

 

'SW 만학도 > C++' 카테고리의 다른 글

9. Inheritance - Advanced and Applications  (0) 2024.04.13
8. Inheritance - Basics  (0) 2024.04.12
6. Out_of_class Definition & Operator Overloading  (0) 2024.04.10
5. Classes  (0) 2024.04.09
4. Functions and Memory Management  (1) 2024.03.29