임시 객체와 이동 시맨틱

2024. 3. 26. 01:11프로그래밍/C++

변환 생성자가 묵시적으로 호출되는 것을 explicit 예약어로 막으려는 이유는 사용자 코드에서 보이지 않는 객체가 생성되고 소멸하는 것을 막기 위함입니다. 그런데 이보다 더 은밀한 임시 객체도 있습니다.

 

참고로 이런 일은 '함수의 반환 형식이 클래스인 경우'에 발생합니다.

 

이름 없는 임시 객체

존재하는 인스턴스지만 '식별자'가 부여되지 안흔 객체를 말합니다.

#include <iostream>
using namespace std;

class CTestData
{
public:
    CTestData(int nParam, char *pszName) : m_nData(nParam), m_pszName(pszName)
    {
        cout <<"CTestData(int): "<<m_pszName<<endl;
    }
    ~CTestData()
    {
        cout<<"~CTEstData(): "<<m_pszName<<endl;
    }

    CTestData(const CTestData &rhs) : m_nData(rhs.m_nData), m_pszName(rhs.m_pszName)
    {
        cout<<"CTestData(const CTestData &): "<<m_pszName<<endl;
    }

    CTestData& operator=(const CTestData &rhs)
    {
        cout<<"operator="<<endl;
        m_nData = rhs.m_nData;
        return *this;
    }

    int GetData() const {return m_nData;}
    void SetData(int nParam) {m_nData=nParam;}
private:
int m_nData=0;
char *m_pszName=nullptr;

};

CTestData TestFunc(int nParam)
{
    //CTestData 클래스 인스턴스인 a는 지역 변수다.
    //따라서 함수가 반환되면 소멸한다.
    CTestData a(nParam, "a");
    return a;
}

int main()
{
    CTestData b(5, "b");
    cout<<"*****Before*****"<<endl;

    //함수가 반환되면서 임시 객체가 생성되었다가 대입 연산 후 즉시 소멸한다;
    b=TestFunc(10);
    cout<<"*****After*****"<<endl;
    cout<<b.GetData() <<endl;

    return 0;
}

 

main()
  |
  |   CTestData b(5, "b");  (1)
  |   -------------------------------
  |   |  m_nData : 5                 |
  |   |  m_pszName : "b"             |
  |   -------------------------------
  |
  |   *****Before*****            (2)
  |   ------------------------------
  |   |   b=TestFunc(10);           |
  |   ------------------------------
  |                | (4)
  |                |
  |                V
  |   TestFunc(10)                (3)
  |   -------------------------------
  |   |  CTestData a(10, "a");      |
  |   -------------------------------
  |
  |   operator= (복사 대입 연산자 호출)   (5)
  |   ---------------------------------
  |   |  m_nData : 10                |
  |   |  m_pszName : "a"             |
  |   ---------------------------------
  |
  |   ~CTEstData(): "a"            (6)
  |
  |   *****After*****             (7)
  |   -------------------------------
  |   |  b                          |
  |   -------------------------------
  |      m_nData : 10
  |      m_pszName : "a"
  |
  -----------------------------------

위 도식은 다음을 나타냅니다:

  1. main() 함수에서 **CTestData b(5, "b");**가 생성됩니다.
  2. "*****Before*****" 메시지가 출력됩니다.
  3. TestFunc(10) 함수가 호출되고, **CTestData a(10, "a");**가 생성됩니다.
  4. TestFunc(10) 함수가 반환된 후, 복사 대입 연산자가 호출되어 **a**의 데이터가 **b**로 복사됩니다.
  5. "*****After*****" 메시지가 출력됩니다.
  6. **a**가 소멸됩니다.
  7. **b**의 데이터가 출력됩니다.

r-value 참조

연산에 따라 생성된 임시 객체

이동 시맨틱(move sementics)

  • 이동 생성자
  • 이동 대입 연산자

임시 객체가 생기는 것은 막을 수가 없다. 그래서 임시 객체가 생성되더라도 부하가 최소화될 수 있도록 구조를 변경한다.

복사 생성자와 대입 연산자에 r-value 참조를 조합해서 새로운 생ㅅ어 및 대입의 경우를 만들어낸다.

#include <iostream>
using namespace std;

class CTestData
{
public:
    CTestData() {cout<<"CTestData()"<<endl;}
	~CTestData() {cout<<"~CTestData()"<<endl;}

	CTestData(const CTestData &rhs) : m_nData(rhs.m_nData)
	{
		cout<<"CTestData(const CTestData &)"<<endl;
	}

	//이동 생성자
	CTestData(CTestData &&rhs) : m_nData(rhs.m_nData)
	{
		cout<<"CTestData(CTestData &&)"<<endl;
	}
	CTestData& operator=(const CTestData &)=default;

	int GetData() const {return m_nData;}
	void SetData(int nParam) {m_nData = nParam;}

private:
    int m_nData=0;
};

CTestData TestFunc(int nParam)
{
	cout<<"**TestFunc(): Begin***"<<endl;

	CTestData a;
    a.SetData(nParam);
	cout<<"**TestFunc(): End*****"<<endl;

	return a;
}

int main()
{
	CTestData b;
	cout<<"*Before*****************"<<endl;
	b = TestFunc(20);
	cout<<"*After*****************"<<endl;
	CTestData c(b);

	return 0;
}

임시 객체는 함수 내부에서 생성되어 함수 외부에서는 직접적으로 접근할 수 없는 객체를 말합니다. TestFunc() 함수에서 반환되는 CTestData a;는 임시 객체입니다. 이 임시 객체는 b = TestFunc(20);에서 b에 할당될 때 이동 생성자를 통해 데이터의 소유권을 b로 이전합니다.

따라서 이 코드에서 이동 생성자는 임시 객체를 b에 할당하여 데이터의 소유권을 전달합니다.

 

main() |

CTestData b; (1)

  main()
    |
    |   CTestData b;  (1)
    |   -------------
    |   |  m_nData  |
    |   -------------
    |
    |   *Before***************** (2)
    |   ------------------------------
    |   |        b = TestFunc(20);    |
    |   ------------------------------
    |               | (4)
    |               |
    |               V
    |   **TestFunc(): Begin***       (3)
    |   ------------------------------
    |   |  CTestData a;                |
    |   |  --------------              |
    |   |  |  m_nData    |             |
    |   |  --------------              |
    |   |                                |
    |   |   SetData(20);                |
    |   ------------------------------
    |               | (5)
    |               |
    |               V
    |   **TestFunc(): End*****         (6)
    |   ------------------------------
    |   |         a (temporary)        |
    |   |  --------------              |
    |   |  |  m_nData    |             |
    |   |  |      20      |             |
    |   |  --------------              |
    |   ------------------------------
    |               | (7)
    |               |
    |               V
    |   *After*****************       (8)
    |   ------------------------------
    |   |        b (moved)             |
    |   |  --------------              |
    |   |  |  m_nData    |             |
    |   |  |      20      |             |
    |   |  --------------              |
    |   ------------------------------
    |               | (9)
    |               |
    |               V
    |   ------------------------------
    |   |        c (copied)            |
    |   |  --------------              |
    |   |  |  m_nData    |             |
    |   |  |      20      |             |
    |   |  --------------              |
    |   ------------------------------
    |
    ------------------------------

위 도식은 다음을 나타냅니다:

  1. main() 함수에서 **CTestData b;**가 생성됩니다.
  2. "*Before*****************" 메시지가 출력됩니다.
  3. TestFunc(20) 함수가 호출되고, "**TestFunc(): Begin***" 메시지가 출력됩니다.
  4. **CTestData a;**가 생성됩니다.
  5. **a.SetData(20);**이 호출됩니다.
  6. "**TestFunc(): End*****" 메시지가 출력되고, a (임시 객체)가 소멸됩니다.
  7. a (임시 객체)가 **b**에 할당될 때 이동 생성자가 호출됩니다.
  8. "*After*****************" 메시지가 출력되고, **b**에는 이동된 데이터가 있습니다.
  9. **b**를 복사하여 **c**가 생성됩니다.


깊은 복사 (Deep Copy)

깊은 복사는 객체의 모든 멤버나 자원을 복사하는 것을 의미합니다. 이는 새로운 메모리 공간이 할당되고, 원본 객체의 자원이나 멤버들이 새로운 객체로 복사되는 것을 의미합니다.

도식:

markdownCopy code
원본 객체:
---------------
| Data: "Hello" |
---------------

깊은 복사:
---------------------------
| Copied Data: "Hello"    |
---------------------------

샘플 코드:

cppCopy code
#include <iostream>#include <cstring> // For strcpyclass DeepCopyExample {
private:
    char* data;
public:
    DeepCopyExample(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // 복사 생성자 (깊은 복사 수행)
    DeepCopyExample(const DeepCopyExample& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    ~DeepCopyExample() {
        delete[] data;
    }

    const char* GetData() const {
        return data;
    }
};

int main() {
    DeepCopyExample original("Hello");
    DeepCopyExample copied(original); // 깊은 복사 수행

    std::cout << "Original data: " << original.GetData() << std::endl;
    std::cout << "Copied data: " << copied.GetData() << std::endl;

    return 0;
}

얕은 복사 (Shallow Copy)

얕은 복사는 단순히 객체의 주소나 포인터만을 복사하는 것을 의미합니다. 이 경우 원본과 복사본이 같은 메모리 자원을 가리키게 됩니다. 따라서 둘 중 하나가 수정될 경우 다른 하나도 영향을 받게 됩니다.

도식:

markdownCopy code
원본 객체:
---------------
| Data: "Hello" |
---------------

얕은 복사:
-------------------
| Copied Data:  --|----\\
-------------------     |
                        |
                        V
-------------------
| Shared Data: "Hello" |
-------------------

샘플 코드:

cppCopy code
#include <iostream>class ShallowCopyExample {
private:
    char* data;
public:
    ShallowCopyExample(const char* str) {
        data = const_cast<char*>(str); // 얕은 복사: 주소 복사
    }

    // 복사 생성자 (얕은 복사 수행)
    ShallowCopyExample(const ShallowCopyExample& other) : data(other.data) {}

    const char* GetData() const {
        return data;
    }
};

int main() {
    ShallowCopyExample original("Hello");
    ShallowCopyExample copied(original); // 얕은 복사 수행

    std::cout << "Original data: " << original.GetData() << std::endl;
    std::cout << "Copied data: " << copied.GetData() << std::endl;

    return 0;
}

이동 생성 (Move Construction)

이동 생성은 객체의 자원을 다른 객체로 이전하는 것입니다. 이는 데이터를 복사하는 대신, 소유권을 이전하여 효율적으로 작동하게 합니다.

도식:

markdownCopy code
원본 객체:
---------------
| Data: "Hello" |
---------------

이동 생성:
---------------------------
| Moved Data: "Hello"       |
---------------------------

샘플 코드:

cppCopy code
#include <iostream>#include <utility> // For std::moveclass MoveConstructorExample {
private:
    char* data;
public:
    MoveConstructorExample(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // 이동 생성자 (원본 객체의 자원을 새 객체로 이전)
    MoveConstructorExample(MoveConstructorExample&& other) : data(std::move(other.data)) {
        other.data = nullptr; // 원본 객체를 초기화하여 무효화
    }

    ~MoveConstructorExample() {
        delete[] data;
    }

    const char* GetData() const {
        return data;
    }
};

int main() {
    MoveConstructorExample original("Hello");
    MoveConstructorExample moved(std::move(original)); // 이동 생성

    std::cout << "Original data: " << original.GetData() << std::endl; // 이동 후에는 무효화됨
    std::cout << "Moved data: " << moved.GetData() << std::endl;

    return 0;
}

 

'프로그래밍 > C++' 카테고리의 다른 글

상속이란  (1) 2024.03.27
묵시적 변환 - 변환 생성자(Conversion Constructor)  (0) 2024.03.25
대입 vs 복사  (0) 2024.03.22
Pass by value VS Pass by reference  (0) 2024.03.21
대입 연산자  (0) 2024.03.21