C ++의 RAII 및 스마트 포인터


193

실제로 C ++에서 RAII 는 무엇이며, 스마트 포인터 는 무엇 이며, 프로그램에서 어떻게 구현되며 스마트 포인터로 RAII를 사용하면 어떤 이점이 있습니까?

답변:


317

RAII의 간단한 (과도하게 사용 된) 예제는 File 클래스입니다. RAII가 없으면 코드는 다음과 같습니다.

File file("/path/to/file");
// Do stuff with file
file.close();

다시 말해, 파일이 끝나면 파일을 닫아야합니다. 여기에는 두 가지 단점이 있습니다. 첫째, File을 사용하는 곳마다 File :: close ()를 호출해야합니다. 두 번째 문제는 파일을 닫기 전에 예외가 발생하면 어떻게됩니까?

Java는 finally 절을 사용하여 두 번째 문제를 해결합니다.

try {
    File file = new File("/path/to/file");
    // Do stuff with file
} finally {
    file.close();
}

또는 Java 7부터 try-with-resource 문 :

try (File file = new File("/path/to/file")) {
   // Do stuff with file
}

C ++은 RAII를 사용하여 두 가지 문제를 모두 해결합니다. 즉, File의 소멸자에서 파일을 닫습니다. File 객체가 적시에 파괴되는 한 (어쨌든) 파일을 닫는 것은 우리를 위해 처리됩니다. 따라서 코드는 다음과 같습니다.

File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us

객체가 언제 소멸되는지 보장 할 수 없기 때문에 Java에서는 수행 할 수 없으므로 파일과 같은 리소스가 언제 해제되는지 보장 할 수 없습니다.

똑똑한 포인터-많은 시간 동안 스택에 객체를 만듭니다. 예를 들어 (그리고 다른 답변에서 예를 훔치는) :

void foo() {
    std::string str;
    // Do cool things to or using str
}

이것은 잘 작동하지만 str을 반환하려면 어떻게해야합니까? 우리는 이것을 쓸 수 있습니다 :

std::string foo() {
    std::string str;
    // Do cool things to or using str
    return str;
}

그래서, 무슨 일이야? 반환 유형은 std :: string이므로 값으로 반환한다는 의미입니다. 이것은 str을 복사하고 실제로 사본을 반환한다는 것을 의미합니다. 비용이 많이들 수 있으므로 복사 비용을 피할 수 있습니다. 따라서 참조 또는 포인터로 반환한다는 아이디어를 얻을 수 있습니다.

std::string* foo() {
    std::string str;
    // Do cool things to or using str
    return &str;
}

불행히도이 코드는 작동하지 않습니다. str에 대한 포인터를 반환하지만 스택에 str이 생성되었으므로 foo ()를 종료하면 삭제됩니다. 다시 말해, 호출자가 포인터를 얻을 때까지는 쓸모가 없습니다 (그리고 사용하면 모든 종류의 펑키 오류가 발생할 수 있기 때문에 쓸모없는 것보다 나쁩니다)

그래서 해결책은 무엇입니까? 우리는 new를 사용하여 힙에 str을 만들 수 있습니다-foo ()가 완료되면 str은 파괴되지 않습니다.

std::string* foo() {
    std::string* str = new std::string();
    // Do cool things to or using str
    return str;
}

물론이 솔루션도 완벽하지 않습니다. 그 이유는 str을 생성했지만 삭제하지 않기 때문입니다. 아주 작은 프로그램에서는 문제가되지 않지만 일반적으로 삭제해야합니다. 우리는 호출자가 객체를 끝내면 객체를 삭제해야한다고 말할 수 있습니다. 단점은 호출자가 메모리를 관리해야하므로 복잡성이 증가하고 잘못되어 메모리 누수가 발생하여 더 이상 필요하지 않더라도 객체가 삭제되지 않는다는 것입니다.

스마트 포인터가 나오는 곳입니다. 다음 예제에서는 shared_ptr을 사용합니다. 실제로 사용하려는 것을 배우기 위해 다양한 유형의 스마트 포인터를 살펴볼 것을 제안합니다.

shared_ptr<std::string> foo() {
    shared_ptr<std::string> str = new std::string();
    // Do cool things to or using str
    return str;
}

이제 shared_ptr은 str에 대한 참조 수를 계산합니다. 예를 들어

shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;

이제 동일한 문자열에 대한 두 개의 참조가 있습니다. str에 대한 나머지 참조가 없으면 삭제됩니다. 따라서 더 이상 직접 삭제하는 것에 대해 걱정할 필요가 없습니다.

빠른 편집 : 일부 의견에서 지적 했듯이이 예제는 두 가지 이유로 완벽하지 않습니다. 첫째, 문자열 구현으로 인해 문자열을 복사하는 것이 저렴한 경향이 있습니다. 둘째, 명명 된 반환 값 최적화라는 이름으로 인해 컴파일러가 작업 속도를 높이기 위해 영리한 작업을 수행 할 수 있으므로 값으로 반환하는 것은 비용이 많이 들지 않을 수 있습니다.

File 클래스를 사용하여 다른 예제를 시도해 봅시다.

파일을 로그로 사용한다고 가정 해 봅시다. 이것은 파일을 추가 전용 모드로 열고 자 함을 의미합니다.

File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log

이제 파일을 몇 가지 다른 객체에 대한 로그로 설정해 보겠습니다.

void setLog(const Foo & foo, const Bar & bar) {
    File file("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

불행히도이 예제는 끔찍하게 끝납니다.이 메소드가 종료 되 자마자 파일이 닫힙니다. 즉, foo와 bar는 이제 유효하지 않은 로그 파일을 가지고 있습니다. 힙에 파일을 생성하고 파일에 대한 포인터를 foo와 bar 모두에 전달할 수 있습니다.

void setLog(const Foo & foo, const Bar & bar) {
    File* file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

그러나 누가 파일 삭제를 담당합니까? 파일을 삭제하지 않으면 메모리와 리소스 누수가 모두 발생합니다. foo 또는 bar가 파일을 먼저 완료할지 여부를 알 수 없으므로 파일 자체를 삭제할 것으로 기대할 수 없습니다. 예를 들어, bar가 파일을 완료하기 전에 foo가 파일을 삭제하면 bar에 유효하지 않은 포인터가 있습니다.

당신이 짐작했듯이, 우리는 똑똑한 포인터를 사용하여 우리를 도울 수 있습니다.

void setLog(const Foo & foo, const Bar & bar) {
    shared_ptr<File> file = new File("/path/to/file", File::append);
    foo.setLogFile(file);
    bar.setLogFile(file);
}

이제 파일 삭제에 대해 걱정할 필요가 없습니다. foo와 bar가 모두 완료되고 더 이상 파일에 대한 참조가 없으면 (foo 및 bar가 손상되어) 파일이 자동으로 삭제됩니다.


7
많은 문자열 구현이 참조 카운트 포인터 측면에서 구현된다는 점에 유의해야합니다. 이러한 COW (Copy-On-Write) 시맨틱은 값을 기준으로 문자열을 반환하는 것이 실제로 저렴합니다.

7
그렇지 않은 컴파일러도 많은 컴파일러가 오버 헤드를 처리하는 NRV 최적화를 구현합니다. 일반적으로 shared_ptr은 거의 유용하지 않습니다. RAII를 고수하고 공유 소유권을 피하십시오.
Nemanja Trifunovic

27
스마트 포인터를 사용하는 것이 문자열을 반환한다고해서 좋은 이유는 아닙니다. 리턴 값 최적화는 리턴을 쉽게 최적화 할 수 있으며 c ++ 1x 이동 시맨틱은 사본을 완전히 제거합니다 (올바르게 사용 된 경우). 실제 사례 (예 : 동일한 리소스를 공유하는 경우) 대신 다음과 같이 표시하십시오.
Johannes Schaub-litb

1
Java가 왜 그렇게 할 수 없는지에 대한 당신의 결론은 명확성이 부족하다고 생각합니다. Java 또는 C #에서이 제한 사항을 설명하는 가장 쉬운 방법은 스택에 할당 할 방법이 없기 때문입니다. C #은 특수 키워드를 통한 스택 할당을 허용하지만 유형 안전을 잃습니다.
ApplePieIsGood

4
@Nemanja Trifunovic :이 문맥에서 RAII는 스택에 복사 / 생성 객체를 반환한다는 의미입니까? 서브 클래 싱 할 수있는 유형의 객체를 반환 / 수락하는 경우에는 작동하지 않습니다. 그런 다음 객체를 자르지 않도록 포인터를 사용해야하며 스마트 포인터가 종종 원시 포인터보다 낫다고 주장합니다.
Frank Osterfeld

141

RAII 단순하지만 멋진 개념의 이상한 이름입니다. SBRM ( Scope Bound Resource Management) 이라는 이름이 더 좋습니다. 아이디어는 종종 블록의 시작 부분에 리소스를 할당하고 블록이 종료 될 때 리소스를 해제해야한다는 것입니다. 블록을 나가는 것은 정상적인 흐름 제어, 점프, 심지어 예외에 의해 발생할 수 있습니다. 이 모든 경우를 다루기 위해 코드가 더 복잡하고 중복됩니다.

SBRM없이 그것을하는 예제 :

void o_really() {
     resource * r = allocate_resource();
     try {
         // something, which could throw. ...
     } catch(...) {
         deallocate_resource(r);
         throw;
     }
     if(...) { return; } // oops, forgot to deallocate
     deallocate_resource(r);
}

보시다시피, 우리는 pwned 할 수있는 방법이 많이 있습니다. 아이디어는 리소스 관리를 클래스로 캡슐화한다는 것입니다. 객체의 초기화는 자원을 획득한다 ( "자원 획득은 초기화이다"). 블록을 종료 할 때 (블록 범위) 리소스가 다시 해제됩니다.

struct resource_holder {
    resource_holder() {
        r = allocate_resource();
    }
    ~resource_holder() {
        deallocate_resource(r);
    }
    resource * r;
};

void o_really() {
     resource_holder r;
     // something, which could throw. ...
     if(...) { return; }
}

리소스를 할당 / 할당 해제하기위한 목적이 아닌 자체 클래스가 있으면 좋습니다. 할당은 작업을 완료하기위한 추가 문제 일뿐입니다. 그러나 자원을 할당 / 할당 해제하려는 즉시 위의 내용은 적합하지 않습니다. 획득 한 모든 종류의 리소스에 대해 래핑 클래스를 작성해야합니다. 이를 용이하게하기 위해 스마트 포인터를 사용하면 해당 프로세스를 자동화 할 수 있습니다.

shared_ptr<Entry> create_entry(Parameters p) {
    shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
    return e;
}

일반적으로 스마트 포인터는 delete소유 한 리소스가 범위를 벗어날 때 호출 되는 새로운 / 삭제 주위의 얇은 래퍼 입니다. shared_ptr과 같은 일부 스마트 포인터를 사용하면 소위 삭제 도구를 말할 수 있습니다 delete. 예를 들어, 올바른 삭제 자에 대해 shared_ptr을 알려주는 한, 창 핸들, 정규식 리소스 및 기타 임의 항목을 관리 할 수 ​​있습니다.

목적에 따라 다른 스마트 포인터가 있습니다.

unique_ptr

객체를 독점적으로 소유하는 스마트 포인터입니다. 부스트는 아니지만 다음 C ++ 표준에 나타날 것입니다. 그것은의 비 복사 가능한 하지만 지원은 전송-의 소유 . 일부 예제 코드 (다음 C ++) :

암호:

unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u

vector<unique_ptr<plot_src>> pv; 
pv.emplace_back(new plot_src); 
pv.emplace_back(new plot_src);

auto_ptr과 달리, unique_ptr은 컨테이너에 넣을 수 있습니다. 컨테이너는 스트림 및 unique_ptr과 같이 복사 할 수없는 (그러나 이동 가능한) 유형을 보유 할 수 있기 때문입니다.

scoped_ptr

복사 및 이동이 불가능한 부스트 스마트 포인터입니다. 범위를 벗어날 때 포인터가 삭제되도록 할 때 사용하는 것이 가장 좋습니다.

암호:

void do_something() {
    scoped_ptr<pipe> sp(new pipe);
    // do something here...
} // when going out of scope, sp will delete the pointer automatically. 

shared_ptr

공유 소유권입니다. 따라서 복사 및 이동이 가능합니다. 여러 스마트 포인터 인스턴스가 동일한 리소스를 소유 할 수 있습니다. 리소스를 소유 한 마지막 스마트 포인터가 범위를 벗어나면 리소스가 해제됩니다. 내 프로젝트 중 하나의 실제 예 :

암호:

shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and 
// plot2 both still have references. 

보시다시피, plot-source (function fx)는 공유되지만 각 항목에는 별도의 항목이 있으며 색상을 설정합니다. 코드가 스마트 포인터가 소유 한 자원을 참조해야하지만 자원을 소유 할 필요는 없을 때 사용되는 weak_ptr 클래스가 있습니다. 원시 포인터를 전달하는 대신 weak_ptr을 작성해야합니다. 더 이상 리소스를 소유 한 shared_ptr이 없더라도 weak_ptr 액세스 경로로 리소스에 액세스하려고하면 예외가 발생합니다.


복사 할 수없는 객체는 가치 의미에 의존하기 때문에 stl 컨테이너에서 전혀 사용하기에 좋지 않습니다. 컨테이너를 정렬하려면 어떻게됩니까? 종류 ... 복사 요소를 않습니다
fmuecke

C ++ 0X 용기는 같은 이동 전용 유형을 존중되도록 변경됩니다 unique_ptr, 및 sort도 마찬가지로 변경됩니다.
요하네스 SCHAUB - litb

SBRM이라는 용어를 처음들은 곳을 기억하십니까? 제임스가 추적하려고합니다.
GManNickG

이것을 사용하기 위해 어떤 헤더 또는 라이브러리를 포함시켜야합니까? 이것에 대한 추가 자료가 있습니까?
atoMerz

여기에 한가지 조언이 있습니다 : @litb에 의한 C ++ 질문에 대한 답변이 있다면, 정답입니다 (투표 또는 "올바른"으로 표시된 답변에 관계없이) ...
fnl

32

전제와 이유는 개념 상 간단합니다.

RAII는 변수가 생성자에서 필요한 모든 초기화와 소멸자에서 필요한 모든 정리를 처리 하도록하는 디자인 패러다임 입니다. 이렇게하면 모든 초기화 및 정리가 단일 단계로 줄어 듭니다.

C ++에는 RAII가 필요하지 않지만 RAII 메소드를 사용하면보다 강력한 코드가 생성된다는 것이 점점 더 인정되고 있습니다.

RAII가 C ++에서 유용한 이유는 C ++가 정상적인 코드 흐름 또는 예외에 의해 트리거 된 스택 해제를 통해 범위에 들어오고 나가는 변수의 생성 및 소멸을 본질적으로 관리하기 때문입니다. 그것은 C ++의 공짜입니다.

모든 초기화 및 정리를 이러한 메커니즘에 연결함으로써 C ++이이 작업을 처리하도록 보장합니다.

C ++에서 RAII에 대해 이야기하면 포인터를 정리할 때 특히 취약하기 때문에 일반적으로 스마트 포인터에 대해 논의하게됩니다. malloc 또는 new에서 얻은 힙 할당 메모리를 관리 할 때 포인터를 제거하기 전에 해당 메모리를 비우거나 삭제하는 것은 프로그래머의 책임입니다. 스마트 포인터는 RAII 철학을 사용하여 포인터 변수가 소멸 될 때마다 힙 할당 개체가 소멸되도록합니다.


또한 포인터는 RAII의 가장 일반적인 응용 프로그램입니다. 다른 리소스보다 수천 배 더 많은 포인터를 할당 할 수 있습니다.
Eclipse

8

스마트 포인터는 RAII의 변형입니다. RAII는 리소스 획득이 초기화 중임을 의미합니다. 스마트 포인터는 사용하기 전에 리소스 (메모리)를 얻은 다음 소멸자에서 자동으로 버립니다. 두 가지 일이 발생합니다.

  1. 메모리 가 마음에 들지 않더라도 항상 사용하기 전에 메모리 를 할당 합니다 . 스마트 포인터로는 다른 방법을 사용하기가 어렵습니다. 이 문제가 발생하지 않으면 NULL 메모리에 액세스하려고 시도하여 충돌이 발생합니다 (매우 아 ful니다).
  2. 우리는 무료 메모리 오류가에도. 메모리가 남아 있지 않습니다.

예를 들어, 다른 예는 네트워크 소켓 RAII입니다. 이 경우 :

  1. 네트워크 소켓은 사용하기 전에 항상 개방되어 있습니다. 기분이 좋지 않더라도 RAII를 사용하여 다른 방식으로 수행하기는 어렵습니다. RAII 없이이 작업을 시도하면 빈 소켓을 열 수 있습니다 (예 : MSN 연결). 그런 다음 "오늘 밤하자"와 같은 메시지가 전송되지 않을 수 있으며 사용자가 배치되지 않으며 해고 될 위험이 있습니다.
  2. 오류가 발생하더라도 네트워크 소켓을 닫습니다 . 소켓이 걸려 있지 않으면 응답 메시지가 "최저 상태 여야합니다"가 발신자를 때리는 것을 막을 수 있습니다.

이제 알 수 있듯이 RAII는 사람들이 배치하는 데 도움이되므로 대부분의 경우 매우 유용한 도구입니다.

스마트 포인터의 C ++ 소스는 저의 응답을 포함하여 인터넷 주위에 수백만입니다.


2

Boost는 공유 메모리에 대한 Boost.Interprocess 의 것들을 포함하여 많은 것들을 가지고 있습니다 . 특히 같은 데이터 구조를 공유하는 5 개의 프로세스가있을 때와 같이 두통을 유발하는 상황에서 메모리 관리를 크게 간소화합니다. delete메모리 누수 또는 실수로 두 번 해제되어 전체 힙을 손상시킬 수있는 포인터가 생기지 않도록 메모리 청크를 담당해야합니다 .


0
무효 foo ()
{
   std :: 문자열 바;
   //
   // 여기에 더 많은 코드
   //
}

무슨 일이 있어도 foo () 함수의 범위가 남아 있으면 bar가 올바르게 삭제됩니다.

내부적으로 std :: string 구현은 종종 참조 카운트 포인터를 사용합니다. 따라서 문자열의 복사본 중 하나가 변경된 경우에만 내부 문자열을 복사하면됩니다. 따라서 참조 횟수 스마트 포인터를 사용하면 필요할 때만 무언가를 복사 할 수 있습니다.

또한 내부 참조 카운팅을 통해 내부 문자열의 사본이 더 이상 필요하지 않을 때 메모리가 올바르게 삭제 될 수 있습니다.


1
무효 f () {Obj x; } Obj x는 스택 프레임 생성 / 파괴 (언 와인딩)를 통해 삭제됩니다. 참조 카운트와는 관련이 없습니다.
Hernán

참조 카운팅은 문자열의 내부 구현 기능입니다. RAII는 개체가 범위를 벗어날 때 개체 삭제의 개념입니다. 문제는 RAII와 스마트 포인터에 관한 것이었다.

1
"어떤 일이 있어도"-함수가 반환되기 전에 예외가 발생하면 어떻게됩니까?
titaniumdecoy

어떤 함수가 반환됩니까? foo에서 예외가 발생하면 bar보다 삭제됩니다. 예외를 던지는 막대의 기본 생성자는 특별한 이벤트입니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.