용어의 의미와 개념 이해-RAII (자원 획득이 초기화 됨)
C ++ 개발자는 RAII의 정의, 중요한 이유 및 다른 언어와 관련이 있는지 여부에 대해 잘 설명해 주시겠습니까?
나는 이렇게 조금 알고있다. 나는 그것이 "자원 획득은 초기화"를 의미한다고 생각합니다. 그러나 그 이름은 RAII가 무엇인지에 대한 나의 이해 (아마도 잘못된)와 함께 움직이지 않습니다 .RAII가 스택에서 객체를 초기화하는 방법이라는 인상을 얻습니다. 그러면 변수가 범위를 벗어날 때 소멸자가 자동으로 리소스를 정리하도록 호출됩니다.
그렇다면 왜 "스택을 사용하여 정리 트리거"(UTSTTC :)라고하지 않습니까? 거기에서 "RAII"로 어떻게 가나 요?
그리고 어떻게 스택에 무언가를 만들어 힙에있는 것을 정리할 수 있습니까? 또한 RAII를 사용할 수없는 경우가 있습니까? 가비지 수집을 원하는 적이 있습니까? 최소한 일부 객체에는 가비지 수집기를 사용하면서 다른 객체는 관리 할 수 있습니까?
감사.
그렇다면 왜 "스택을 사용하여 정리 트리거"(UTSTTC :)라고하지 않습니까?
RAII가 수행 할 작업을 알려줍니다. 생성자에서 리소스를 확보하십시오! 리소스 하나, 생성자 하나를 추가합니다. UTSTTC는 그 중 하나 일 뿐이며 RAII는 그 이상입니다.
자원 관리가 짜증납니다. 여기서 리소스는 사용 후 정리가 필요한 것입니다. 많은 플랫폼에서 진행된 프로젝트 연구에 따르면 대부분의 버그는 리소스 관리와 관련이 있으며 특히 많은 유형의 개체 및 할당 자 때문에 Windows에서 나쁩니다.
C ++에서 예외와 (C ++ 스타일) 템플릿의 조합으로 인해 리소스 관리가 특히 복잡합니다. 후드 아래를 살펴 보려면 GOTW8을 참조하십시오 .
C ++ 은 생성자가 성공한 경우에만 소멸자가 호출되도록 합니다 . 이에 의존하여 RAII는 일반 프로그래머가 알지 못하는 불쾌한 많은 문제를 해결할 수 있습니다. 다음은 "내가 돌아올 때마다 지역 변수가 파괴 될 것"을 넘어 몇 가지 예입니다.
FileHandle
RAII를 사용 하는 지나치게 단순한 클래스 부터 시작하겠습니다 .
class FileHandle
{
FILE* file;
public:
explicit FileHandle(const char* name)
{
file = fopen(name);
if (!file)
{
throw "MAYDAY! MAYDAY";
}
}
~FileHandle()
{
// The only reason we are checking the file pointer for validity
// is because it might have been moved (see below).
// It is NOT needed to check against a failed constructor,
// because the destructor is NEVER executed when the constructor fails!
if (file)
{
fclose(file);
}
}
// The following technicalities can be skipped on the first read.
// They are not crucial to understanding the basic idea of RAII.
// However, if you plan to implement your own RAII classes,
// it is absolutely essential that you read on :)
// It does not make sense to copy a file handle,
// hence we disallow the otherwise implicitly generated copy operations.
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// The following operations enable transfer of ownership
// and require compiler support for rvalue references, a C++0x feature.
// Essentially, a resource is "moved" from one object to another.
FileHandle(FileHandle&& that)
{
file = that.file;
that.file = 0;
}
FileHandle& operator=(FileHandle&& that)
{
file = that.file;
that.file = 0;
return *this;
}
}
생성이 실패하면 (예외 제외) 소멸자도 아닌 다른 멤버 함수도 호출되지 않습니다.
RAII는 유효하지 않은 상태의 오브젝트 사용을 피합니다. 객체를 사용하기 전에 이미 인생을 더 쉽게 만듭니다.
이제 임시 객체를 살펴 보겠습니다.
void CopyFileData(FileHandle source, FileHandle dest);
void Foo()
{
CopyFileData(FileHandle("C:\\source"), FileHandle("C:\\dest"));
}
처리해야 할 세 가지 오류 사례가 있습니다. 파일을 열 수없고 하나의 파일 만 열 수 있으며 두 파일을 모두 열 수는 있지만 파일 복사에 실패했습니다. 비 RAII 구현에서는 Foo
세 가지 경우를 모두 명시 적으로 처리해야합니다.
RAII는 한 명령문 내에서 여러 자원을 확보 한 경우에도 획득 한 자원을 해제합니다.
이제 몇 가지 객체를 집계 해 보겠습니다.
class Logger
{
FileHandle original, duplex; // this logger can write to two files at once!
public:
Logger(const char* filename1, const char* filename2)
: original(filename1), duplex(filename2)
{
if (!filewrite_duplex(original, duplex, "New Session"))
throw "Ugh damn!";
}
}
의 생성자는 Logger
경우 실패 할 것 original
'(때문에 생성자가 실패 filename1
열 수 없습니다), duplex
(때문에 생성자 실패'를 filename2
열 수 없습니다), 또는 내부의 파일에 쓰기 Logger
의 생성자 몸이 실패합니다. 이러한 경우의에서 Logger
의 소멸자는 것입니다 하지 호출 - 우리가 의존 할 수 있도록 Logger
'파일을 해제 소멸자를. 그러나 생성 된 경우 original
생성자를 정리하는 동안 소멸자가 호출됩니다 Logger
.
RAII는 부분 구성 후 정리를 단순화합니다.
부정적인 점 :
부정적인 점? RAII 및 스마트 포인터로 모든 문제를 해결할 수 있습니다. ;-)
지연된 획득이 필요할 때 집계 된 개체를 힙으로 밀어 넣어 RAII가 다루기 어려운 경우가 있습니다.
로거가 필요하다고 상상해보십시오 SetTargetFile(const char* target)
. 이 경우 여전히의 구성원이어야하는 핸들 Logger
은 힙에 있어야합니다 (예 : 스마트 포인터 등).
가비지 수집을 정말로 원하지 않았습니다. C #을 수행 할 때 가끔 걱정할 필요가없는 행복한 순간을 느끼지만 결정 론적 파괴를 통해 만들어 질 수있는 모든 멋진 장난감을 그리워합니다. ( IDisposable
단지 사용 하지 마십시오.)
"간단한"스마트 포인터가 여러 클래스에 대한 순환 참조를 발생시키는 GC의 이점을 얻을 수있는 특히 복잡한 구조가 하나 있습니다. 우리는 강하고 약한 포인터의 균형을 신중하게 조정하여 혼란을 겪었지만 무언가를 바꾸고 싶을 때마다 큰 관계형 차트를 연구해야합니다. GC가 더 나을 수도 있지만 일부 구성 요소에는 최대한 빨리 릴리스해야하는 리소스가 있습니다.
FileHandle 샘플에 대한 참고 사항 : 완성되지는 않았으며 샘플만이 아니라 잘못된 것으로 나타났습니다. 지적한 Johannes Schaub와 올바른 C ++ 0x 솔루션으로 전환 한 FredOverflow에 감사드립니다. 시간이 지남에 따라 여기에 문서화 된 접근 방식으로 정착했습니다 .
거기에는 훌륭한 답변이 있으므로 잊어 버린 몇 가지를 추가하십시오.
0. RAII는 범위에 관한 것입니다
RAII는 다음과 같습니다.
- 생성자에서 리소스 (어떤 리소스에 관계없이)를 획득하고 소멸자에서이를 확보하지 않습니다.
- 변수가 선언되면 생성자가 실행되고 변수가 범위를 벗어날 때 소멸자가 자동으로 실행됩니다.
다른 사람들은 이미 그것에 대해 대답했기 때문에 자세히 설명하지 않습니다.
1. Java 또는 C #으로 코딩 할 때 이미 RAII를 사용하고 있습니다.
MONSIEUR JOURDAIN : 뭐! 내가 "니콜, 슬리퍼를 가져다주고 나이트캡을 줘"라고 말할 때 그것은 산문입니까?
필로소피 마스터 : 네.
MONSIEUR JOURDAIN : 40 년 이상 동안 나는 그것에 대해 아무 것도 모르고 산문을 해왔으며, 그 사실을 가르쳐 주신 것에 대해 여러분은 많은 의무를지고 있습니다.
— Molière : 중산층 신사, 2 막, 장면 4
Monsieur Jourdain이 산문을했듯이 C # 및 Java 사용자도 이미 RAII를 사용하지만 숨겨진 방식으로 사용합니다. 예를 들어, (C # 대체하여 동일한 방법으로 기록 된 다음 자바 코드 synchronized
로 lock
) :
void foo()
{
// etc.
synchronized(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
... 이미 RAII를 사용하고 있습니다. 뮤텍스 획득이 키워드 ( synchronized
또는 lock
)에서 수행되고 범위를 종료 할 때 비 획득이 수행됩니다.
RAII에 대해 들어 본 적이없는 사람들조차도 설명이 거의 필요하지 않습니다.
C ++이 Java 및 C #보다 장점은 RAII를 사용하여 무엇이든 만들 수 있다는 것입니다. 예를 들어, 거기에 직접 빌드에서의 동등하지 synchronized
않고 lock
++ C에서, 그러나 우리는 여전히있을 수 있습니다.
C ++에서는 다음과 같이 작성됩니다.
void foo()
{
// etc.
{
Lock lock(someObject) ; // lock is an object of type Lock whose
// constructor acquires a mutex on
// someObject and whose destructor will
// un-acquire it
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
Java / C # 방식으로 쉽게 작성할 수 있습니다 (C ++ 매크로 사용).
void foo()
{
// etc.
LOCK(someObject)
{
// if something throws here, the lock on someObject will
// be unlocked
}
// etc.
}
2. RAII는 다른 용도로 사용됩니다
WHITE RABBIT : [노래] 늦었 어 / 늦었 어 / 아주 중요한 데이트. / "안녕하세요"라고 말할 시간이 없습니다. / 안녕. / 늦었 어 늦었 어 늦었 어
— 이상한 나라의 앨리스 (Disney version, 1951)
생성자가 언제 (객체 선언에서) 호출되는지, 해당 소멸자가 호출 될 때 (스코프의 끝에서) 알기 때문에 거의 마법적인 코드를 한 줄로 작성할 수 있습니다. C ++ 원더 랜드에 오신 것을 환영합니다 (적어도 C ++ 개발자의 관점에서).
예를 들어, 위의 잠금 객체가 사용 된 것처럼 카운터 객체를 작성하고 (운동으로 보자) 변수를 선언하여 사용할 수 있습니다.
void foo()
{
double timeElapsed = 0 ;
{
Counter counter(timeElapsed) ;
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter's declaration till the scope exit
}
물론 매크로를 사용하여 Java / C # 방식으로 다시 작성할 수 있습니다.
void foo()
{
double timeElapsed = 0 ;
COUNTER(timeElapsed)
{
// do something lengthy
}
// now, the timeElapsed variable contain the time elapsed
// from the Counter's declaration till the scope exit
}
3. C ++에는 왜 부족 finally
합니까?
[음성] 마지막 카운트 다운입니다!
— 유럽 : 최종 카운트 다운 (죄송합니다. 인용문이 없습니다.
이 finally
절은 C # / Java에서 범위 종료의 경우 ( return
또는 예외를 통해) 리소스 처리를 처리하는 데 사용됩니다 .
Astute 사양 독자들은 C ++에 finally 절이 없다는 것을 알게 될 것입니다. RAII는 이미 리소스 폐기를 처리하므로 C ++에는 필요하지 않기 때문에 이것은 오류가 아닙니다. (그리고 C ++ 소멸자를 작성하는 것은 올바른 Java finally 절을 작성하는 것보다 심지어 C #의 올바른 Dispose 메소드를 작성하는 것보다 훨씬 쉽습니다.)
그러나 때로는 finally
절이 시원 할 것입니다. C ++로 할 수 있습니까? 응 우리는 할 수있어! 그리고 다시 RAII를 대신 사용합니다.
결론 : RAII는 C ++의 철학 이상입니다. C ++입니다.
RAII? 이것은 C ++입니다 !!!
— C++ developer's outraged comment, shamelessly copied by an obscure Sparta king and his 300 friends
When you reach some level of experience in C++, you start thinking in terms of RAII, in terms of construtors and destructors automated execution.
You start thinking in terms of scopes, and the {
and }
characters become ones of the most important in your code.
And almost everything fits right in terms of RAII: exception safety, mutexes, database connections, database requests, server connection, clocks, OS handles, etc., and last, but not least, memory.
The database part is not negligible, as, if you accept to pay the price, you can even write in a "transactional programming" style, executing lines and lines of code until deciding, in the end, if you want to commit all the changes, or, if not possible, having all the changes reverted back (as long as each line satisfy at least the Strong Exception Guarantee). (see the second part of this Herb's Sutter article for the transactional programming).
And like a puzzle, everything fits.
RAII is so much part of C++, C++ could not be C++ without it.
This explains why experienced C++ developers are so enamored with RAII, and why RAII is the first thing they search when trying another language.
And it explains why the Garbage Collector, while a magnificient piece of technology in itself, is not so impressive from a C++ developer's viewpoint:
- RAII already handles most of the cases handled by a GC
- A GC deals better than RAII with circular references on pure managed objects (mitigated by smart uses of weak pointers)
- Still A GC is limited to memory, while RAII can handle any kind of resource.
- As described above, RAII can do much, much more...
Please see:
Do programmers of other languages, besides C++, use, know or understand RAII?
RAII and smart pointers in C++
Does C++ support 'finally' blocks? (And what's this 'RAII' I keep hearing about?)
etc..
RAII is using C++ destructors semantics to manage resources. For example, consider a smart pointer. You have a parameterized constructor of the pointer that initializes this pointer with the adress of object. You allocate a pointer on stack:
SmartPointer pointer( new ObjectClass() );
When the smart pointer goes out of scope the destructor of the pointer class deletes the connected object. The pointer is stack-allocated and the object - heap-allocated.
There are certain cases when RAII doesn't help. For example, if you use reference-counting smart pointers (like boost::shared_ptr) and create a graph-like structure with a cycle you risk facing a memory leak because the objects in a cycle will prevent each other from being released. Garbage collection would help against this.
I concur with cpitis. But would like to add that the resources can be anything not just memory. The resource could be a file, a critical section, a thread or a database connection.
It is called Resource Acquisition Is Initialization because the resource is acquired when the object controlling the resource is constructed, If the constructor failed (ie due to an exception) the resource is not acquired. Then once the object goes out of scope the resource is released. c++ guarantees that all objects on the stack that have been successfully constructed will be destructed (this includes constructors of base classes and members even if the super class constructor fails).
The rational behind RAII is to make resource acquisition exception safe. That all resources acquired are properly released no matter where an exception occurs. However this does rely on the quality of the class that acquires the resource (this must be exception safe and this is hard).
I'd like to put it a bit more strongly then previous responses.
RAII, Resource Acquisition Is Initialization means that all acquired resources should be acquired in the context of the initialization of an object. This forbids "naked" resource acquisition. The rationale is that cleanup in C++ works on object basis, not function-call basis. Hence, all cleanup should be done by objects, not function calls. In this sense C++ is more-object oriented then e.g. Java. Java cleanup is based on function calls in finally
clauses.
The problem with garbage collection is that you lose the deterministic destruction that's crucial to RAII. Once a variable goes out of scope, it's up to the garbage collector when the object will be reclaimed. The resource that's held by the object will continue to be held until the destructor gets called.
RAII comes from Resource Allocation Is Initialization. Basically, it means that when a constructor finishes the execution, the constructed object is fully initialized and ready to use. It also implies that the destructor will release any resources (e.g. memory, OS resources) owned by the object.
Compared with garbage collected languages/technologies (e.g. Java, .NET), C++ allows full control of the life of an object. For a stack allocated object, you'll know when the destructor of the object will be called (when the execution goes out of the scope), thing that is not really controlled in case of garbage collection. Even using smart pointers in C++ (e.g. boost::shared_ptr), you'll know that when there is no reference to the pointed object, the destructor of that object will be called.
And how can you make something on the stack that will cause the cleanup of something that lives on the heap?
class int_buffer
{
size_t m_size;
int * m_buf;
public:
int_buffer( size_t size )
: m_size( size ), m_buf( 0 )
{
if( m_size > 0 )
m_buf = new int[m_size]; // will throw on failure by default
}
~int_buffer()
{
delete[] m_buf;
}
/* ...rest of class implementation...*/
};
void foo()
{
int_buffer ib(20); // creates a buffer of 20 bytes
std::cout << ib.size() << std::endl;
} // here the destructor is called automatically even if an exception is thrown and the memory ib held is freed.
When an instance of int_buffer comes into existence it must have a size, and it will allocate the necessary memory. When it goes out of scope, it's destructor is called. This is very useful for things like synchronization objects. Consider
class mutex
{
// ...
take();
release();
class mutex::sentry
{
mutex & mm;
public:
sentry( mutex & m ) : mm(m)
{
mm.take();
}
~sentry()
{
mm.release();
}
}; // mutex::sentry;
};
mutex m;
int getSomeValue()
{
mutex::sentry ms( m ); // blocks here until the mutex is taken
return 0;
} // the mutex is released in the destructor call here.
Also, are there cases where you can't use RAII?
No, not really.
Do you ever find yourself wishing for garbage collection? At least a garbage collector you could use for some objects while letting others be managed?
Never. Garbage collection only solves a very small subset of dynamic resource management.
There are already a lot of good answers here, but I'd just like to add:
A simple explanation of RAII is that, in C++, an object allocated on the stack is destroyed whenever it goes out of scope. That means, an objects destructor will be called and can do all necessary cleanup.
That means, if an object is created without "new", no "delete" is required. And this is also the idea behind "smart pointers" - they reside on the stack, and essentially wraps a heap based object.
RAII is an acronym for Resource Acquisition Is Initialization.
This technique is very much unique to C++ because of their support for both Constructors & Destructors & almost automatically the constructors that are matching that arguments being passed in or the worst case the default constructor is called & destructors if explicity provided is called otherwise the default one that is added by the C++ compiler is called if you didn't write an destructor explicitly for a C++ class. This happens only for C++ objects that are auto-managed - meaning that are not using the free store (memory allocated/deallocated using new,new[]/delete,delete[] C++ operators).
RAII technique makes use of this auto-managed object feature to handle the objects that are created on the heap/free-store by explcitly asking for more memory using new/new[], which should be explicitly destroyed by calling delete/delete[]. The auto-managed object's class will wrap this another object that is created on the heap/free-store memory. Hence when auto-managed object's constructor is run, the wrapped object is created on the heap/free-store memory & when the auto-managed object's handle goes out of scope, destructor of that auto-managed object is called automatically in which the wrapped object is destroyed using delete. With OOP concepts, if you wrap such objects inside another class in private scope, you wouldn't have access to the wrapped classes members & methods & this is the reason why smart pointers (aka handle classes) are designed for. These smart pointers expose the wrapped object as typed object to external world & there by allowing to invoke any members/methods that the exposed memory object is made up of. Note that smart pointers have various flavors based on different needs. You should refer to Modern C++ programming by Andrei Alexandrescu or boost library's (www.boostorg) shared_ptr.hpp implementation/documentation to learn more about it. Hope this helps you to understand RAII.
'IT박스' 카테고리의 다른 글
스프링 부트 및 여러 외부 구성 파일 (0) | 2020.08.05 |
---|---|
빠른. (0) | 2020.08.05 |
#if C #에서 디버그하지 않습니까? (0) | 2020.08.05 |
디렉토리를 재귀 적으로 복사하는 배치 파일 (0) | 2020.08.05 |
AlertDialog에서 사용하기 위해 뷰를 부 풀릴 때 "뷰 루트로 null을 전달하지 마십시오"경고 (0) | 2020.08.05 |