Step 5 of 7
RAII wrappers for C libraries
Professional C++ constantly calls C libraries: OS APIs, databases, compression, crypto. They hand out raw handles that you must release with a specific function: FILE* with fclose, sqlite3* with sqlite3_close, and so on. Forgetting, or releasing twice, is the same class of bug as malloc/free.
The fix is to wrap each handle immediately in a std::unique_ptr with a custom deleter: a small type whose operator() releases the handle. Then it's released on every path, exactly once, automatically.
#include <cstdio>
#include <iostream>
#include <memory>
struct FileCloser {
void operator()(std::FILE* f) const {
if (f) {
std::fclose(f);
std::cout << "(closed)\n";
}
}
};
using File = std::unique_ptr<std::FILE, FileCloser>;
int count_lines(const char* path) {
File f(std::fopen(path, "r"));
if (!f) return -1; // nothing to close; early return is safe
int n = 0;
for (int c; (c = std::fgetc(f.get())) != EOF; ) {
if (c == '\n') n++;
}
return n; // f closes itself here
}
int main() {
{
File out(std::fopen("data.txt", "w"));
std::fputs("one\ntwo\nthree\n", out.get());
} // closed (and flushed) here
std::cout << count_lines("data.txt") << " lines\n";
std::cout << count_lines("missing.txt") << "\n";
}
(closed)
(closed)
3 lines
-1
How it works
std::unique_ptr<std::FILE, FileCloser>is a unique_ptr whose second template argument says how to delete. Instead ofdelete, it callsFileCloser{}(ptr).f.get()gives the rawFILE*for the C functions.- An empty
unique_ptrnever calls the deleter, so a failedfopen(null) needs no special handling. Theif (f)check inside the deleter is extra safety. - The
Filealias keeps the long type readable.
extern "C": calling between C and C++
C++ encodes each function's parameter types into its name in the compiled code (name mangling, which is how overloading works), and C doesn't. So when C++ code calls a function that was compiled as C, it has to be told, or the linker looks for the wrong name and reports undefined reference. C libraries' headers do this themselves:
#ifdef __cplusplus
extern "C" {
#endif
int sqlite3_close(sqlite3 *db);
#ifdef __cplusplus
}
#endif
__cplusplus is only defined by C++ compilers, so C sees a plain declaration and C++ sees it inside extern "C". The same keyword works in the other direction: an extern "C" function written in C++ can be called from C, and from Python, Rust or any other language that can call C, as long as its parameters and return type are plain C types. The standard C headers (<cstdio> and friends) already handle this, which is why the example above calls std::fopen directly.
Your task
open_file(path, mode)returnsFile(std::fopen(path.c_str(), mode.c_str()))(or with whatever parameter types you choose).first_lineopens the file, returnsstd::nulloptif that fails, then reads characters withfgetcuntil'\n'orEOF. If nothing at all was read (an empty file), returnstd::nullopt.
Your turn: write the FileCloser, the File alias, File open_file(path, mode), and std::optional<std::string> first_line(path), which returns the file's first line (without the newline) or std::nullopt if it can't be opened or is empty. No manual fclose in first_line.