C/C++ Arena

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

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

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.

Previous: Pimpl Next: Type erasure