C++. 15.의미론적 이동과, 스마트포인터

이도현·2023년 8월 9일
0

1. 객체 복사 생성(copy-construction)

  • 객체를 복사할 때는 새로운 자원이 할당되고, 원본 객체와 복사된 객체는 완전히 구분돼야 한다.
class MyClass {
public:
    MyClass(const MyClass& other) { /* 복사 생성 로직 */ }
};

2. 두 객체의 교환(swapping)

  • 이동 의미론이 추가되기 전에는 swapping이 자원 할당이나 복사 없이 데이터를 전달 하는 일반적인 방법이었다. 객체는 단순하게 내용을 서로 교환한다.
void swap(MyClass& a, MyClass& b) { /* 교환 로직 */ }

3. 객체 이동 생성

  • 객체를 이동할 때는 생성된 객체가 원본 객체의 자원을 가져간다. 그 후 원본 객체가 초기화 된다.
class MyClass {
public:
    MyClass(MyClass&& other) { /* 이동 생성 로직 */ }
};

4. 3의 법칙(the rule of three)

  • c++에서 클래스와 자원 획득의 기본적인 개념 중 하나는 클래스가 반드시 자신의 자원을 완벽하게 관리해야 한다는 것이다.
  • 관리란 복사 할당 해제 3가지 기능을 구연해야 한다고 해서 3의 법칙이다.
class MyClass {
public:
    MyClass(const MyClass& other); // 복사 생성자
    MyClass& operator=(const MyClass& other); // 복사 할당 연산자
    ~MyClass(); // 소멸자
};

5. 3의 법칙의 문제점

  • 복사할 수 없는 자원: std::thread 클래스를 포함한 자원, 네트워크 연결 등을 객체러 넘기는 것은 불가능
  • 불필요한 복사: Buffer 클래스를 반환하는 경우, 전체 배열이 복사돼야 한다.

6. 이동 의미론

  • 3의 법칙을 해결하기 위해 등장했으며 5의 법칙으로 확장

    복사 생성자(copy-constructor)
    복사 할당(copy-assignment)
    소멸자(destructor)
    이동 생성자(move-constructor)
    이동 할당(move-assignment)

  • 이동생성자/ 이동할당은 실제로 할당하거나 예외를 발생시킬만한 작업을 수행하지 않는다.
  • 명시적으로 기재된 복사 생성자, 복사 할당, 이동 생성자, 이동 할당, 소멸자가 필요없는 자신만의 클래스를 작성하는 것을 0의 법칙이라고 한다.
class MyClass {
public:
    MyClass(MyClass&& other); // 이동 생성자
    MyClass& operator = (MyClass&& other); // 이동 할당 연산자
    // 복사 생성자, 복사 할당 연산자, 소멸자 포함
};

7. 스마트포인터

  • 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제해 줍니다.

8. 이동의 의미와 스마트 포인터

  • RAII(resource acquistion is initialization)
  • 최신 c++은 RAII 원칙에 따라 리소스 누출을 방지

Syntax vs semantics

  • syntax: 문장이 언어의 문법에 따라 유효한지 또는 타당한지 아닌지 확인하는 것과 관련이 있다.
  • semantics: 문장이 타당한 의미를 지니는지 아닌지를 판별하는 것과 관련이 있다.

9. 오른쪽 값 참조

  • 임시 객체와 같은 r-valuew를 참조할 수 있게 한다.
void process(MyClass&& rref) { /* r-value 참조 로직처리*/ }

10. 이동생성자 이동 대입

  • 이동 생성자와 이동 대입 연산자는 객체의 자원을 효율적이로 이전
MyClass::MyClass(MyClass&& other) { /* 이동 생성 로직 */ }
MyClass& MyClass::operator=(MyClass&& other) { /* 이동 할당 로직 */ }

11. std::move

  • std::move를 사용하여 l-values를 r-values로 캐스팅하고, 이동 의미론을 사용할 수 있게 합니다.
MyClass a;
MyClass b = std::move(a); // 'a'의 자원을 'b'로 이동

12. std::unique_ptr(중요)

  • 할당된 객체를 가르키는 포인터
  • 가장 주요한 특징은 소유권
  • 스코프를 벗어나면 객체는 자동으로 삭제
#include <iostream>
#include <memory> // for std::unique_ptr

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    void sayHi() { std::cout << "Hi\n"; }
};

int main() {
    {
        std::unique_ptr<Resource> res(new Resource()); // Resource 객체를 생성하고 그 소유권을 res에 할당

        res->sayHi(); // res를 통해 Resource의 멤버 함수 사용

        // res는 자동으로 메모리를 해제합니다. 명시적으로 delete를 호출할 필요가 없습니다.
    } // res가 스코프를 벗어나면서 Resource 객체는 자동으로 소멸됩니다.

    std::cout << "End of main\n";

    // Resource가 이미 소멸되었기 때문에 아무 일도 일어나지 않습니다.
    return 0;
}
  • unique_ptr의 -> 연산자를 사용하여 Resoucre에 접근할 수 있다.

13. std::shared_ptr

  • 유니크포인터와 달리 소유권을 여러 곳에서 공유가능한 포인터
#include <iostrem>
#include "Resource.h"

int main(){
	Resource *res = new Resource(3);
    res -> setAll(1);
    std::shared_ptr<Resource> ptr1(res); # 몇군데에서 공유되고 있는지 센다.
    
    // auto ptr1 = std::make_shared<Resource>(3); 이 방법이 더 많이 사용된다.
    // ptr1 -> setAll(1);
    
    ptr -> print();
    {
    	std::shared_ptr<Resource> ptr2(ptr1);
        
        ptr2->setAll(3);
        ptr2->print();
        
        std::cout << "Going out of the block" << std::endl;
    }
    
    ptr1 -> print();
    
    std::cout << "Going out of the outer block << std::endl;
    
}
  • std::make_shared는 new를 호출하는 것보다 더 선호된다. 왜냐하면 메모리 효율성, 예외 안정성, 성능최적화, 간결함을 가진다.

14. 순환 의존성문제와 std::weak_ptr

  • shared_ptr은 객체에 대한 소유권을 공유하며, 참조 카운트가 0이 되면 해당 객체를 자동으로 삭제
  • 하지만, 두 객체가 서로 shared_ptr을 통해 가리킬 대 발생하는 것이 순환 의존성
  • 각 객체가 서로의 참조 카운트를 증가시키기 때문에, 지역을 벗어나며 디스트로이 되지않고 메모리 누수 발생
  • 해결방법은 std::weak_ptr사용
#include <iostream>
#include <memory>

class B; // 전방 선언

class A {
public:
    std::weak_ptr<B> b_ptr; // 약한 참조로 변경
    ~A() { std::cout << "A 소멸" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 약한 참조로 변경
    ~B() { std::cout << "B 소멸" << std::endl; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b_ptr = b;
    b->a_ptr = a; // 약한 참조로 인해 순환 의존성 해결

    return 0; // 메모리 누수 없음, A와 B가 정상적으로 소멸
}
  • std::weak_ptr은 객체가 여전히 종재하는지 확인하기 위해 std::shared_ptr로 변환할 수 있는 lock함수를 제공
  • lock함수는 객체가 이미 소멸된 경우 std::shared_ptr를 반환
profile
좋은 지식 나누어요

0개의 댓글