This article looks at handling errors in C APIs using an all-singing-all-dancing approach that uses a fatted up RAII variant.

Introduction

I mentioned that there are variants of RAII (that include the slim RAII), and this describes a variant that adds more complexity in the classes that wrap C APIs with the goal of making it easier to use.

The first issue is that copying a handle is not always possible or cheap to do, so usually, like in our classic RAII file class copying (constructor and assignment) are disabled. However it would be useful if the class could be moved.

To make the class movable, in addition to implementing the move constructor and assignment, we need to test for an invalid handle in the destructor before releasing the resource. Remember that in the classical RAII, that was not required, but the move semantics render the source object unusable: with an invalid handle.

1
2
3
4
5
6
7
8
9
10
11
12
13
file::file(file && other) noexcept :
  f_{ other.f_ }
{
  other.f_ = nullptr;
}

file::~file()
{
  if (f_)
  {
    fclose(f_)
  }
}

Making our RAII class movable makes it easy to return it directly from functions and store it in containers. In particular it’s easier to use a class if we define factory functions with names, rather than overloading the constructor. A file could be opened, or could be one of the 3 predefined values (input, output or error).

1
2
auto x = file::open("file.name", "rb");
auto y = file::stdout();

When standard files are used there is also the question if we want to close them when the instance goes out of scope. To handle the option of not closing the file, I’ve added the own_ boolean as a member variable.

Full code

file.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#pragma once

#include <cstdio>

class file
{
  FILE * f_;
  bool own_;

  file(FILE * f, bool own);
public:
  file(const file &) = delete;
  file & operator=(const file &) = delete;
  file(file && other) noexcept;
  file & operator=(file && other) noexcept;

  ~file();

  static file open(const char * file_name, const char * mode);
  static file stdout(bool own = false);

  size_t read(char * buffer, size_t size);
  void write(const char * buffer, size_t size);
  bool is_eof();

private:
  static void log_and_throw(const char * message);
};

file.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include "file.h"
#include <stdexcept>

file::file(FILE * f, bool own) :
  f_{ f },
  own_{ own }
{
}

file::file(file && other) noexcept :
  f_{ other.f_ },
  own_{ other.own_ }
{
  other.own_ = false;
}

file & file::operator=(file && other) noexcept
{
  if (this != &other)
  {
    if (own_)
    {
      fclose(f_);
    }
    f_ = other.f_;
    own_ = other.own_;
    other.own_ = false;
  }
  return *this;
}

file::~file()
{
  if (own_)
  {
    fclose(f_);
  }
}

file file::open(const char * file_name, const char * mode)
{
  FILE * f{ fopen(file_name, mode) };
  if ( ! f)
  {
    log_and_throw("Failed to open file");
  }
  return file{ f, true };
}

file file::stdout(bool own)
{
  return file{ ::stdout, own };
}

size_t file::read(char * buffer, size_t size)
{
  size_t read_count{ fread(buffer, 1, size, f_) };
  if ((read_count != size) && ferror(f_))
  {
    log_and_throw("Failed to read from file");
  }
  return read_count;
}

void file::write(const char * buffer, size_t size)
{
  size_t write_count{ fwrite(buffer , 1, size, f_) };
  if (write_count != size)
  {
    log_and_throw("Failed to write to file");
  }
}

bool file::is_eof()
{
  return feof(f_);
}

void file::log_and_throw(const char * message)
{
  perror(0);
  throw std::runtime_error(message);
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "file.h"
#include <iostream>

void copy_file()
{
  auto src = file::open("src.bin", "rb");
  auto dst = file::stdout();
  char buffer[1024];

  do
  {
    size_t read_count = src.read(buffer, sizeof(buffer));

    if (read_count > 0)
    {
      dst.write(buffer, read_count);
      std::cout << '.';
    }
  } while ( ! src.is_eof());
}

int main ()
{
  try
  {
    copy_file();

    std::cout << "\nSUCCESS\n";
    return 0;
  }
  catch(const std::exception & e)
  {
    std::cerr << e.what() << std::endl;
    return 1;
  }
}

Summary

As you use a particular RAII class more and more, there might be a trade off to be made between compactness and minimalism (with impact on performance) one one side (classic RAII) and ease of use on the other side (the example above).