Ramblings about things

Smart file pointer in C++

When writing C++ and having to interoperate with the C standard library I always get a somewhat nostalgic feeling; yearning for simpler times1, times that never were since I've never really written C for a living. And pretty much every time I think to myself - "But surely, there's gotta be a better way of doing this!"2

One of such things I've come across several times already is handling files and the icky raw pointer that fopen returns. Of course we want to fclose it properly, which means it's a prime candidate for RAII by wrapping it into a smart pointer!

Let's start with the naïve approach:

using FilePtr = std::unique_ptr< std::FILE, decltype(&fclose) >;

FilePtr openFile( std::string_view const path, std::string_view const mode ) {
    return {
        std::fopen( path.data(), mode.data() ),
        &std::fclose
    };
}

This pretty much solves the problem we set out to solve. We can use it semi-haphazardly and it should be fine3. That's how I've always done it, it seems elegant enough to write it once and forget about it, which I did, until I switched over to GCC for unrelated reasons and got a warning4:

| warning: ignoring attributes on template argument ‘int (*)(FILE*)’ [-Wignored-attributes]
|     using FilePtr = std::unique_ptr< FILE, decltype( &std::fclose )>;
|                                                                    ^

The curiosity got the better of me so instead of just suppressing it, I wanted to find out what does it actually mean. Based on this stack overflow post, it's most likely the nonnull attribute; when using the pointer as a template parameter its attributes get discarded, but that's not the only notable thing - using a function pointer as a deleter increases the size of the smart pointer!

Let's check that for ourselves (godbolt link):

using FilePtrDeleter = std::unique_ptr< std::FILE, decltype( &std::fclose ) >;
using FilePtr        = std::unique_ptr< std::FILE                           >;

static_assert( sizeof(FilePtr) == sizeof(FilePtrDeleter) )

/*
<source>:6:32: error: static assertion failed
    6 | static_assert( sizeof(FilePtr) == sizeof(FilePtrDeleter) );
      |                ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:6:32: note: the comparison reduces to '(8 == 16)'
*/

Well I guess the internet is right! The passed function pointer has to be stored somewhere for later dereference and invocation, but by using a custom deleter function instead of a function pointer we should avoid that (godbolt link):

constexpr auto deleter = [](std::FILE * fstream) -> int {
    return std::fclose( fstream );
};

using FilePtrDeleter = std::unique_ptr< std::FILE, decltype( deleter ) >;
using FilePtr        = std::unique_ptr< std::FILE                      >;

static_assert( sizeof(FilePtr) == sizeof(FilePtrDeleter) );

No errors! Here's this blog post godbolted.5

Conclusion

What seemed elegant and simple when I first did it actually had a hidden flaw, which I wouldn't have noticed for who knows how long were it not for experimenting and trying to satisfy my own curiosity.

Moral of the story - follow the interesting-looking breadcrumbs, you never know where they might lead you. Maybe a new knowledge nugget, maybe nothing, or maybe even to a blog post like this one!


  1. Before the Stockholm syndrome took over.

  2. People who have worked with me will either groan or gleam at this.

  3. Your mileage may vary.

  4. A warning that I (in)conveniently cannot reproduce on godbolt with the same code. But trust me, it's there!

  5. Yes, it's a verb, and you won't convince me otherwise!

#cpp #raii #smart_ptr