본문 바로가기

프로그래밍/Modern C++

[C++] RAII(Resoucre Acquisition Is Initialization)

 

 

 

RAII

RAII는 Resoucre Acquisition Is Initialization(리소스 획득=초기화)의 줄임말이다.

RAII는 간단하지만 상당히 막강한 개념으로서, RAII 인스턴스가 스코프를 벗어나면 할당했던 리소스를 자동으로 해제하는 데 활용된다.  이렇게 동작하는 시점은 미리 알 수 있다. 기본적으로 새로운 RAII 인스턴스의 생성자가 특정한 리소스에 대한 소유권을 획득해서 그 리소스에 대한 인스턴스를 초기화 하는 것이다. 그래서  Resoucre Acquisition Is Initialization 라고 부른다. RAII 인스턴스가 사라지면 소멸자는 획득했던 리소스를 자동으로 해제한다. 

 

핵심: Heap에 할당된 자원은 명시적으로 해제하지 않으면 해제가 안되지만 지역변수의 경우 스코프를 벗어나면 소멸자(Destructor)가 호출되는 것을 이용한 것이다.

 

RAII가 필요한 이유

다음과 같은 경우를 보자 이 경우는 어느정도 C++을 해본 사람이라면 메무리 누수(memory Leak)이 발생한다는 것을 알 수 있다.

그러면 Function() 밑에 delete 연산자를 추가해서 진행해보자

이러면 정상 종료된다고 생각할 수 있다. 하지만 기대와 다르게 메모리 누수의 가능성은 아직도 남아있다. 

그 이유는 Functino()함수에서 예외(Exception)가 발생하면 delete 가 실행되지 않기 때문이다.

 

 

RAII File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
class File {
private:
    std::FILE* file_;
public:
    //복사 생성자와, 복사 대입 연산자를 허용하지 않는다
    File(const File&= delete;
    File& operator=(const File&= delete;
    // 이동 생성과, 이동 대입 연산자는 허용한다.
    File( File&&= default;
    File& operator=( File&&= default;
    File(std::FILE* file):file_(file) {
    }
    ~File() {
        std::cout << "Call Destructor Function\n";
        Reset();
    }
    //Get(), Release(), reset()
    std::FILE* Get()const noexcept {
        std::cout << "Call Get Function\n";
        return file_;
    }
    std::FILE* Release() noexcept {
        std::cout << "Call Release Function\n";
        std::FILE* file = file_;
        file_ = nullptr;
        return file;
    }
    void Reset(std::FILE* file = nullptr)noexcept {
        std::cout << "Call Reset Function\n";
        if (file_) {
            delete file_;
        }
        file_ = file;
    }
};
int main() {
    File myfile(fopen("Temp.txt""r"));
}
 
 

RAII Mutex

RAII 클래스를 정의할 때 디폴트 생성자를 추가하거나 디폴트 생성자를 명시적으로 delete 하지 않는 것이 좋다. 왜 그런지는 표준 라이브러리의 RAII클래스인 std::unique_lock()을 직접 사용해보면 알 수 있다.

해당 함수는 RAII인 unique_lock의 객체를 이용하여 데이터 멤버에 락을 걸었다가 메서드가 끝날 즈음 자동으로 락을 해제하는 로컬 lock 객체를 생성한다. 그런데 이 lock 객체를 정의한 뒤 직접 사용하지 않기 때문에 다음과 같은 실수를 저지르기 쉽다.

 

이렇게 해도 컴파일은 되지만 의도와 다르게 실행된다. 이렇게하면 lock_ 로컬 변수를 선언하고 unique_lock의 디폴트생성자를 호출해서 이 변수를 초기화한다. 따라서 데이터 멤버인 lock_에 대해 락이 걸리지 않는다.

 

핵심: RAII 클래스에 절대로 디폴트 생성자를 두지 않는다.

 

후기

RAII에 대해서 책에 있는 내용을 간단하게 정리 하였다.  C++은 따로 가비지 컬렉터가 없기 때문에 메모리를 할당하고 지우는거에 실수가 발생하기 마련이다. 이러한 것을 방지하기 위해 스마트 포인터 등 다양한 포인터가 최신 C++에서 지원되고 있고 이러한 스마트포인터는 RAII기반으로 구현이 된다는 것을 알 수 있었다.

 

출처 및 레퍼런스

Book: 전문가를 위한 C++ 17