A file handle

Borrowing from llfio::file_handle which uses this design pattern1, here is a simplified file_handle implementation:

class file_handle
{
  int _fd{-1};  // file descriptor
  struct stat _stat
  {
    0
  };  // stat of the fd at open

  // Phase 1 private constexpr constructor
  constexpr file_handle() {}

public:
  using path_type = filesystem::path;

  //! The behaviour of the handle: does it read, read and write, or atomic append?
  enum class mode : unsigned char  // bit 0 set means writable
  {
    unchanged = 0,
    none = 2,        //!< No ability to read or write anything, but can synchronise (SYNCHRONIZE or 0)
    attr_read = 4,   //!< Ability to read attributes (FILE_READ_ATTRIBUTES|SYNCHRONIZE or O_RDONLY)
    attr_write = 5,  //!< Ability to read and write attributes (FILE_READ_ATTRIBUTES|FILE_WRITE_ATTRIBUTES|SYNCHRONIZE or O_RDONLY)
    read = 6,        //!< Ability to read (READ_CONTROL|FILE_READ_DATA|FILE_READ_ATTRIBUTES|FILE_READ_EA|SYNCHRONISE or O_RDONLY)
    write = 7,       //!< Ability to read and write (READ_CONTROL|FILE_READ_DATA|FILE_READ_ATTRIBUTES|FILE_READ_EA|FILE_WRITE_DATA|FILE_WRITE_ATTRIBUTES|FILE_WRITE_EA|FILE_APPEND_DATA|SYNCHRONISE or O_RDWR)
    append = 9       //!< All mainstream OSs and CIFS guarantee this is atomic with respect to all other appenders (FILE_APPEND_DATA|SYNCHRONISE or O_APPEND)
  };

  // Moves but not copies permitted
  file_handle(const file_handle &) = delete;
  file_handle(file_handle &&o) noexcept : _fd(o._fd) { o._fd = -1; }
  file_handle &operator=(const file_handle &) = delete;
  file_handle &operator=(file_handle &&o) noexcept
  {
    this->~file_handle();
    new(this) file_handle(std::move(o));
    return *this;
  }
  // Destruction closes the handle
  ~file_handle()
  {
    if(_fd != -1)
    {
      if(-1 == ::close(_fd))
      {
        int e = errno;
        std::cerr << "FATAL: Closing the fd during destruction failed due to " << strerror(e) << std::endl;
        std::terminate();
      }
      _fd = -1;
    }
  }

  // Phase 2 static member constructor function, which cannot throw
  static inline outcome::result<file_handle> file(path_type path, mode mode = mode::read) noexcept;
};
View this code on Github

Note the default member initialisers, these are particularly convenient for implementing phase 1 of construction. Note also the constexpr constructor, which thanks to the default member initialisers is otherwise empty.

File handles are very expensive to copy as they involve a syscall to duplicate the file descriptor, so we enable moves only.

The destructor closes the file descriptor if it is not -1, and if the close fails, seeing as there is nothing else we can do without leaking the file descriptor, we fatal exit the process.

Finally we declare the phase 2 constructor which is a static member function.


  1. LLFIO uses Outcome “in anger”, both in Standard and Experimental configurations. If you would like a real world user of Outcome to study the source code of, it can be studied at https://github.com/ned14/llfio/. [return]