This article looks at handling errors in C APIs using an if-error-return coding pattern which uses a slimmed down RAII variant which is sometimes useful.

Introduction

I mentioned that there are variants of RAII, and this describes the slimmed down variant that uses very simple classes to wrap resources and ensure they are released.

This approach is useful for example when working with an unfamiliar C API. Say at the stage when you’re not sure: should fread belong to the file class or to the buffer class? This is a serious question, because there is a ‘smell’ in code that if a function member of a class needs lots of member variables from another class, maybe it belongs to the wrong class. fread needs both the pointer to and the size of data from the buffer, and only the file pointer from the file.

In this slimmed down RAII variant we have a simple class that gets the resource on the constructor and frees it in the destructor. It is so slimmed down that the resource is accessible as a public member variable, and there no other member functions. To even remove the need to declare public accessibility the class is actually a struct.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct file
{
  FILE * p;

  explicit file(FILE * x) noexcept :
    p{ x }
  {
  }

  ~file()
  {
    if (p)
    {
      fclose(p);
    }
  }
};

To initialize and use this class one needs to use the if-error-return pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file src{ fopen("src.bin", "rb") };
if ( ! src.p)
{
  perror("Failed to open source file");
  return 1;
}

// more
// code
// here

size_t read_count = fread(buffer.p, 1, buffer_size, src.p);
if ((read_count != buffer_size) && ferror(src.p))
{
  perror("Failed to read from source file");
  return 1;
}

Another situation where this approach might be useful is where you need more control over if and when exceptions are thrown.

Issues

The issue with this approach is the repetition, that it shares with all the if-error patterns. While resources are released by the destructor, the repetition for the if blocks is still a major issue. Every time we call some function we need to repeat:

  • the if condition
  • the error handling
  • the return statement
  • and 2 curly brackets

Full code

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
84
85
86
87
88
89
90
91
92
93
#include <stdio.h>
#include <stdlib.h>

struct file
{
  FILE * p;

  explicit file(FILE * x) noexcept :
    p{ x }
  {
  }

  ~file()
  {
    if (p)
    {
      fclose(p);
    }
  }

  file(const file &) = delete;
  file & operator=(const file &) = delete;
};

struct mem_buffer
{
  char * p;

  explicit mem_buffer(char * x) noexcept :
    p{ x }
  {
  }

  ~mem_buffer()
  {
    if (p)
    {
      free(p);
    }
  }

  mem_buffer(const mem_buffer &) = delete;
  mem_buffer & operator=(const mem_buffer &) = delete;
};

int main ()
{
  file src{ fopen("src.bin", "rb") };
  if ( ! src.p)
  {
    perror("Failed to open source file");
    return 1;
  }

  file dst{ fopen("dst.bin", "wb") };
  if ( ! dst.p)
  {
    perror("Failed to open destination file");
    return 1;
  }

  constexpr size_t buffer_size{ 1024 };
  mem_buffer buffer{ reinterpret_cast<char *>(malloc(buffer_size)) };
  if ( ! buffer.p)
  {
    fputs("Failed to allocate buffer\n", stderr);
    return 1;
  }

  do
  {
    size_t read_count = fread(buffer.p, 1, buffer_size, src.p);
    if ((read_count != buffer_size) && ferror(src.p))
    {
      perror("Failed to read from source file");
      return 1;
    }

    if (read_count > 0)
    {
      size_t write_count = fwrite(buffer.p, 1, read_count, dst.p);
      if (write_count != read_count)
      {
        perror("Failed to write to destination file");
        return 1;
      }
      fputs(".", stdout);
    }
  } while ( ! feof(src.p));

  fputs("\nSUCCESS\n", stdout);
  return 0;
}

Summary

Use it as a stopgap measure to ensure resources are released while learning a C API or in a very localized area handling a C API.