RAII

RAII is the rule that a resource is owned by an object: the constructor acquires it, the destructor releases it, and nothing else has to remember either step. A file, a lock, a socket, a heap block, a temporary directory: all of them can be wrapped this way.

The point is not the destructor itself but the guarantee it gives. Whatever path the function takes, the object's lifetime ends at the closing brace, and the release runs. Lesson 016 described when objects die; this lesson puts that moment to work.

A file that closes itself

The class below wraps a C file handle. The constructor opens the file, the destructor closes it, and write() is the only operation the rest of the program needs. The handle is private, so no caller can close it early or leak it.

The program creates the object inside a block, writes one line and prints a marker. The destructor runs at the closing brace, before the second marker, and the output shows the order.

Nothing in main mentions fclose. That is the whole idea: the release is tied to the object's lifetime, not to a line of code the author has to remember.

The third command prints the file, so the transcript shows both halves of the deal: the destructor closed the handle, and the text really reached disk. A failed open leaves the handle null, and every operation checks it.

#include <cstdio> #include <iostream> class file { public: explicit file(const char* path) : handle_(std::fopen(path, "w")) { if (handle_ == nullptr) { std::cout << "could not open " << path << '\n'; } } ~file() { if (handle_ != nullptr) { std::fclose(handle_); std::cout << "closed\n"; } } void write(const char* text) { if (handle_ != nullptr) { std::fputs(text, handle_); } } private: std::FILE* handle_; }; int main() { { file out{"note.txt"}; out.write("hello from RAII\n"); std::cout << "inside the block\n"; } std::cout << "after the block\n"; }

g++ -std=c++23 -Wall -Wextra file.cpp -o file_demo
./file_demo
cat note.txt

inside the block
closed
after the block
hello from RAII

An early return still releases

A function may return from several places. With a local RAII object, every one of those returns destroys it, so the resource is released even when the function leaves before its last line.

The function below opens a file, decides to stop after one line and returns. The destructor still runs, and the second marker prints after the close.

The manual version of this function needs a close before every return, and the case that is forgotten is the one that leaks. The compiler writes the close for you when the destructor does it.

#include <cstdio> #include <iostream> class file { public: explicit file(const char* path) : handle_(std::fopen(path, "w")) {} ~file() { if (handle_ != nullptr) { std::fclose(handle_); std::cout << "closed in destructor\n"; } } void write(const char* text) { if (handle_ != nullptr) { std::fputs(text, handle_); } } private: std::FILE* handle_; }; void write_one_line() { file out{"early.txt"}; out.write("one line\n"); std::cout << "returning early\n"; return; } int main() { write_one_line(); std::cout << "back in main\n"; }

g++ -std=c++23 -Wall -Wextra early_return.cpp -o early_return
./early_return

returning early
closed in destructor
back in main

Several resources, reverse order

When one function owns several resources, each is destroyed in the reverse order of construction. The last resource acquired is the first released, which is what a program wants when the later resource depends on the earlier one.

The program opens two files in one block and writes a marker between them. The close messages arrive in the opposite order to the opens, and the file names make the pair visible.

This is the same rule as lesson 025 showed for objects in a block, applied to resources that matter.

#include <cstdio> #include <iostream> class file { public: explicit file(const char* path) : name_(path), handle_(std::fopen(path, "w")) { std::cout << "opened " << name_ << '\n'; } ~file() { if (handle_ != nullptr) { std::fclose(handle_); std::cout << "closed " << name_ << '\n'; } } private: const char* name_; std::FILE* handle_; }; int main() { file first{"first.txt"}; file second{"second.txt"}; std::cout << "both open\n"; }

g++ -std=c++23 -Wall -Wextra nested.cpp -o nested
./nested

opened first.txt
opened second.txt
both open
closed second.txt
closed first.txt

A guard for something that is not an object

Some clean-up is not a resource with an open and a close: restoring a flag, printing a summary, rolling back a partial change. A scope guard turns any function into work that runs when the block ends.

The class below stores a function pointer and calls it in the destructor. The program uses it to print a line when main returns, and the line appears after the message from the body.

The same pattern with a lambda instead of a function pointer is the usual modern form; lambdas are the subject of lesson 052.

#include <iostream> class on_exit { public: explicit on_exit(void (*fn)()) : fn_(fn) {} ~on_exit() { fn_(); } private: void (*fn_)(); }; void report() { std::cout << "guard ran at the end of main\n"; } int main() { on_exit guard{report}; std::cout << "body of main\n"; }

g++ -std=c++23 -Wall -Wextra guard.cpp -o guard
./guard

body of main
guard ran at the end of main

An owner must not be copied

A class that owns a handle must decide what a copy means. The compiler-generated copy copies the pointer, so two objects close the same file: the second close is undefined behaviour.

The fix is to delete the copy operations, which turns the mistake into a compile error. The program below tries to copy a file object, and the compiler reports that the copy constructor is deleted.

A class that should be copyable needs a copy that duplicates the resource, which is lesson 031. A class that should be movable needs a move that transfers it, which is lesson 032.

Deleting a function is also documentation. A reader of the class sees that copying is not part of the design, and a caller who tries it gets a compile error at the call rather than a crash in the destructor much later.

#include <cstdio> class file { public: explicit file(const char* path) : handle_(std::fopen(path, "w")) {} file(const file&) = delete; file& operator=(const file&) = delete; ~file() { if (handle_ != nullptr) { std::fclose(handle_); } } private: std::FILE* handle_; }; int main() { file original{"copy.txt"}; file copy = original; return 0; }

g++ -std=c++23 -Wall -Wextra copy_error.cpp -o copy_error

copy_error.cpp: In function 'int main()':
copy_error.cpp:25:17: error: use of deleted function 'file::file(const file&)'
25 | file copy = original;
| ^~~~~~~~
copy_error.cpp:8:5: note: declared here
8 | file(const file&) = delete;
| ^~~~
copy_error.cpp:25:17: note: use '-fdiagnostics-all-candidates' to display considered candidates
25 | file copy = original;
| ^~~~~~~~

Exercises

  1. Add a read_line() method to the file class and call it from a block. When is the file closed relative to the printed line?
  2. Write a scope guard that increments a counter, and check the counter after the block ends.
  3. Remove the deleted copy operations from copy_error.cpp and compile. Which two objects would own the same handle?

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does RAII mean?

    Answer

    A resource is acquired by a constructor and released by a destructor

    The lifetime of the object is tied to the lifetime of the resource, so no path can forget the release.

  2. What happens to the resource when a function returns early?

    Answer

    The destructor of the local object still runs

    The destructor runs at the end of the scope on every path, including the early return.

  3. In what order are two nested guards destroyed?

    Answer

    The second one first, then the first

    Locals are destroyed in the reverse of their construction, which is what the nested program shows.

  4. Why must an owning class delete its copy constructor?

    Answer

    A copy would release the same resource twice

    Two objects with one resource both free it, which is a double release.

  5. What does this program print? #include <iostream> struct guard { ~guard() { std::cout << "closed\n"; } }; int main() { { guard g; std::cout << "inside the block\n"; } std::cout << "after the block\n"; }

    Answer

    inside the block / closed / after the block

    The destructor runs at the end of the block and prints its line before the next statement.

  6. What does this program print? #include <iostream> struct guard { ~guard() { std::cout << "closed in destructor\n"; } }; void early() { guard g; std::cout << "returning early\n"; } int main() { early(); std::cout << "back in main"; }

    Answer

    returning early / closed in destructor / back in main

    The local is destroyed on the return path, before the caller continues.

  7. What does this program print? #include <iostream> struct file { const char* name; file(const char* n) : name(n) { std::cout << "opened " << name << '\n'; } ~file() { std::cout << "closed " << name << '\n'; } }; int main() { file first{"first.txt"}; file second{"second.txt"}; std::cout << "both open\n"; }

    Answer

    opened first.txt / opened second.txt / both open / closed second.txt / closed first.txt

    The two files are opened in order and closed in reverse at the end of the block.

  8. What does this program print? #include <iostream> #include <functional> struct cleanup { std::function<void()> action; ~cleanup() { action(); } }; int main() { cleanup guard{[] { std::cout << "guard ran at the end of main\n"; }}; std::cout << "body of main\n"; }

    Answer

    body of main / guard ran at the end of main

    The guard is a local whose destructor runs the stored action when main returns.

  9. What does copying an owner that deleted its copy constructor produce?

    Answer

    use of deleted function 'file::file(const file&)'

    The deleted function is still a candidate, and the compiler reports that it is deleted.

  10. Why does the guard's action run at the end of main?

    Answer

    The guard is a local object, and its destructor runs at the end of its scope

    The destructor call is inserted by the compiler at the closing brace of the scope.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev