24

I'm a C programmer learning C++. In C, there is a common goto idiom used to handle errors and exit cleanly from a function. I've read that exception handling via try-catch blocks is preferred in object-oriented programs, but I'm having trouble implementing this paradigm in C++.

Take for example the following function in C which uses the goto error handling paradigm:

unsigned foobar(void){
    FILE *fp = fopen("blah.txt", "r");
    if(!fp){
        goto exit_fopen;
    }

    /* the blackbox function performs various
     * operations on, and otherwise modifies,
     * the state of external data structures */
    if(blackbox()){
        goto exit_blackbox;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data = malloc(NUM_DATUM*sizeof(*data));
    if(!data){
        goto exit_data;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            goto exit_read;
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        printf("%lu\n", data[i] + data[i + NUM_DATUM/2]);
    }

    free(data)
    /* the undo_blackbox function reverts the
     * changes made by the blackbox function */
    undo_blackbox();
    fclose(fp);

    return 0;

exit_read:
    free(data);
exit_data:
    undo_blackbox();
exit_blackbox:
    fclose(fp);
exit_fopen:
    return 1;
}

I tried to recreate the function in C++ using the exception handling paradigm as such:

unsigned foobar(){
    ifstream fp ("blah.txt");
    if(!fp.is_open()){
        return 1;
    }

    try{
        // the blackbox function performs various
        // operations on, and otherwise modifies,
        // the state of external data structures
        blackbox();
    }catch(...){
        fp.close();
        return 1;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data;
    try{
        data = new unsigned long [NUM_DATUM];
    }catch(...){
        // the undo_blackbox function reverts the
        // changes made by the blackbox function
        undo_blackbox();
        fp.close();
        return 1;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;
        if(!getline(fp, buffer)){
            delete[] data;
            undo_blackbox();
            fp.close();
            return 1;
        }

        stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    delete[] data;
    undo_blackbox();
    fp.close();

    return 0;
}

I feel my C++ version didn't properly implement the exception handling paradigm; in fact, the C++ version seems even less readable and more error prone due to a build of cleanup code accumulating in the catch blocks as the function grows.

I've read that all this cleanup code in the catch blocks may be unnecessary in C++ due to something called RAII, but I'm unfamiliar with the concept. Is my implementation proper, or is there a better way to handle errors and cleanly exit a function in C++?

Community
  • 1
  • 1
Vilhelm Gray
  • 11,516
  • 10
  • 61
  • 114
  • 3
    If the size of `data` is a compile-time constant of small size, why are you dynamically allocating it? If you want to, use `std::vector` which doesn't need to be explicitly deleted, no matter how the function returns or throws. You also don't need to explicitly close the file stream. Put your whole function in a try block, and depending on some logic (a flag or catch different exception types) decide whether to call undo_blackbox. – Neil Kirk Feb 20 '15 at 14:25
  • 6
    Definetly look up RAII. For example, your `fp.close()` is unnecessary. Using an std::vector, your data deletion should become obsolete, too. – nvoigt Feb 20 '15 at 14:26
  • @NeilKirk This is a forced example: I wanted to have the possibility of a memory allocation exception. – Vilhelm Gray Feb 20 '15 at 14:27
  • 16
    The point of RAII is this: you don't need to call `fp.close()` when `blackbox()` throws (or at all, in fact) - `ifstream`'s destructor will do this for you. Use a [scope guard](http://en.wikibooks.org/wiki/More_C++_Idioms/Scope_Guard) to automatically run `undo_blackbox()` on abnormal exit. Make `data` an `std::vector` - its destructor will automatically deallocate memory, whether the function exits normally or via an exception. Once you manage all resources via RAII, you would very rarely write `try/catch` block at all - you would just allow exceptions to propagate. – Igor Tandetnik Feb 20 '15 at 14:27
  • http://programmers.stackexchange.com/questions/46592/so-what-did-alan-kay-really-mean-by-the-term-object-oriented – William Pursell Feb 20 '15 at 14:28
  • 1
    Before you go implementing RAII patterns all over the show, I think it is wise to stop and ask yourself "is it reasonable to run this cleanup code when an unhandled exception is being propagated up the stack?" This pattern has always struck me as very dangerous! If you expected the exception, you'd handle it. So if the exception is unhandled, it is probably unexpected, so *you don't know the state of your own program*. Is it really wise to run a whole bunch more code in this scenario? It's like a bomb goes off at lunch, and before you evacuate the building you wash your dishes. – Eric Lippert Feb 20 '15 at 23:55
  • 1
    @EricLippert well the only other reasonable thing to do is to abort... You don't throw an exception because of corrupted state, you abort when you have corrupted state. Exceptions are for non-corrupted but invalid state (like allocating memory when there's no free memory; not like allocating memory when the free-block list is corrupted). – user253751 Feb 21 '15 at 08:15
  • @EricLippert: Exceptions *do* belong to your program's state. What you are talking about sounds more like an assertion failure, at least in C++. I know that other languages, e.g. Java, are very lax about assertions and treat them as exceptions. This is not the case in C++. – Christian Hackl Feb 21 '15 at 09:30
  • @immibis: well, there is "crash-only software": http://lwn.net/Articles/191059/, so aborting may indeed be a reasonable option. – ninjalj Feb 21 '15 at 11:43
  • @ninjalj that's a different thing, I think. – user253751 Feb 21 '15 at 12:18

4 Answers4

31

The principle of RAII is that you use a class type to manage any resource which needs cleaning up after use; that cleanup is done by the destructor.

That means you can create a local RAII manager, which will automatically clean up whatever it's managing when it goes out of scope, whether that's due to normal program flow or an exception. There should never be any need for a catch block just to clean up; only when you need to handle or report the exception.

In your case, you have three resources:

  • The file fp. ifstream is already an RAII type, so just remove the redundant calls to fp.close() and all is good.
  • The allocated memory data. Use a local array if it's a small fixed size (as this is), or std::vector if it needs to be dynamically allocated; then get rid of the delete.
  • The state set up by blackbox.

You can write your own RAII wrapper for the "black box" malarkey:

struct blackbox_guard {
    // Set up the state on construction
    blackbox_guard()  {blackbox();}

    // Restore the state on destruction
    ~blackbox_guard() {undo_blackbox();}

    // Prevent copying per the Rule of Three
    blackbox_guard(blackbox_guard const &) = delete;
    void operator=(blackbox_guard) = delete;
};

Now you can remove all your error-handling code; I'd indicate failure through exceptions (either thrown, or allowed to propagate) rather than a magic return value, giving:

void foobar(){
    ifstream fp ("blah.txt"); // No need to check now, the first read will fail if not open
    blackbox_guard bb;

    const size_t NUM_DATUM = 42;
    unsigned long data[NUM_DATUM];   // or vector<unsigned long> data(NUM_DATUM);

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;

        // You could avoid this check by setting the file to throw on error
        // fp.exceptions(ios::badbit); or something like that before the loop
        if(!getline(fp, buffer)){
             throw std::runtime_error("Failed to read"); // or whatever
        }

        stringstream(buffer) >> data[i]; // or data[i] = stoul(buffer);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }
}
Community
  • 1
  • 1
Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
  • 2
    This is admittedly the wrong forum but how does the exception/RAII model tend to play out in practice in large-ish applications? The last time I tried it I tended to forget about the implicit code paths and get the system into an inconsistent state after rare exceptions. On the flip-side I've also frequently been known to get the manual clean-up and error condition tests wrong. – doynax Feb 20 '15 at 15:07
  • 13
    @doynax: In my experience, it's the most reliable model for largish applications - you can't ignore an exception, or forget to clean up a resource. The implicit control paths are only a problem if you didn't use RAII correctly in the first place; that is, if you use types that don't provide at least a [basic exception guarantee](http://en.wikipedia.org/wiki/Exception_safety). Use RAII consistently, and exceptions will be your friends. Don't, and non-exceptional flow (returns, breaks, etc.) can also be a problem, albeit more visible than exceptions. – Mike Seymour Feb 20 '15 at 15:13
  • @MikeSeymour since you're using c++11 anyway... it's rule of five not three, the RAII wrapper shouldn't be movable either. – Mgetz Feb 20 '15 at 15:21
  • 1
    @doynax I don't think there is any doubt that the RAII model is the most reliable in large-ish applications but there is nothing that says you have to use exceptions just because you are using RAII. I've seen both an exception model and a return value model used successfully on largeish applications. – Chris Drew Feb 20 '15 at 15:21
  • 8
    @Mgetz: It's still the Rule of Three for correctness (you only need to worry about move semantics if you want to make them different from copy semantics). Deleting the copy operations will also delete the move operations, so my wrapper is neither copyable not movable. – Mike Seymour Feb 20 '15 at 15:22
  • 2
    @ChrisDrew: Indeed, RAII doesn't force the use of exceptions. I prefer exceptions, since return values can be (and in my experience sometime are) ignored, leading to subtle bugs that exceptions would have exposed immediately; others have other opinions. But the question is about how to use RAII, not whether to use exceptions. – Mike Seymour Feb 20 '15 at 15:29
  • @MikeSeymor: The scary part wasn't so much the outright crashes or leaks but a couple of bugs resulting in partial updates to the application data after exceptions, effectively trashing the user's data. Should exceptions be coupled with more of a transactional model to avoid this? (Admittedly I initially also got into massive trouble when attempting to mix-and-match methods.) – doynax Feb 20 '15 at 15:43
  • 1
    @doynax I would suggest [Watching John Kalb's talk on Exception Safe Code](http://youtu.be/W7fIy_54y-w) – Mgetz Feb 20 '15 at 15:45
  • 2
    @doynax: Yes, you'll need a [strong exception guarantee](http://en.wikipedia.org/wiki/Exception_safety) when making state changes. I'd recommend avoiding mutable state, but we're already getting quite far off-topic. – Mike Seymour Feb 20 '15 at 15:48
  • @doynax: now that you talk about transactional, http://www.gotw.ca/gotw/061.htm talks about exception safety in terms of ACID transactional guarantees. – ninjalj Feb 21 '15 at 11:55
10

Yes, you should use RAII (Resource Acquisition Is Initialisation) wherever possible. It leads to code which is both easy to read and safe.

The core idea is that you acquire resources during the initialisation of an object, and set up the object so that it correctly releases the resources on its destruction. The vital point why this works is that destructors run normally when scope is exited via an exception.

In your case, there is already RAII available and you're just not using it. std::ifstream (I assume that's what your ifstream refers to) indeed closes on destruction. So all the close() calls in catch can safely be omitted and will happen automatically—precisely what RAII is for.

For data, you should be using an RAII wrapper too. There are two available: std::unique_ptr<unsigned long[]>, and std::vector<unsigned long>. Both take care of memory deallocation in their respective destructors.

Finally, for blackbox(), you can create a trivial RAII wrapper yourself:

struct BlackBoxer
{
  BlackBoxer()
  {
    blackbox();
  }

  ~BlackBoxer()
  {
    undo_blackbox();
  }
};

When rewritten with these, your code would become much simpler:

unsigned foobar() {
  ifstream fp ("blah.txt");
  if(!fp.is_open()){
    return 1;
  }

  try {
    BlackBoxer b;

    const size_t NUM_DATUM = 42;
    std::vector<unsigned long> data(NUM_DATUM);
    for(size_t i = 0; i < NUM_DATUM; i++){
      string buffer;
      if(!getline(fp, buffer)){
        return 1;
      }

      stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
      cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    return 0;
  } catch (...) {
    return 1;
  }
}

Additionally, notice that your function uses a return value to indicate success or failure. This may be what you want (if failure is "normal" for this function), or might just represent only going half-way (if failure is supposed to be exceptional too).

If it's the latter, simply change the function to void, get rid of the trycatch construct, and throw a suitable exception instead of return 1;.

Finally, even if you decide to keep the return value approach (which is perfectly valid), consider changing the function to return bool, with true meaning success. It's more idiomatic.

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
7

Let me rewrite that for you using c++ idiom with explanations inline to the code

// void return type, we may no guarantees about exceptions
// this function may throw
void foobar(){
   // the blackbox function performs various
   // operations on, and otherwise modifies,
   // the state of external data structures
   blackbox();

   // scope exit will cleanup blackbox no matter what happens
   // a scope exit like this one should always be used
   // immediately after the resource that it is guarding is
   // taken.
   // but if you find yourself using this in multiple places
   // wrapping blackbox in a dedicated wrapper is a good idea
   BOOST_SCOPE_EXIT[]{
       undo_blackbox();
   }BOOST_SCOPE_EXIT_END


   const size_t NUM_DATUM = 42;
   // using a vector the data will always be freed
   std::vector<unsigned long> data;
   // prevent multiple allocations by reserving what we expect to use
   data.reserve(NUM_DATUM);
   unsigned long d;
   size_t count = 0;
   // never declare things before you're just about to use them
   // doing so means paying no cost for construction and
   // destruction if something above fails
   ifstream fp ("blah.txt");
   // no need for a stringstream we can check to see if the
   // file open succeeded and if the operation succeeded
   // by just getting the truthy answer from the input operation
   while(fp >> d && count < NUM_DATUM)
   {
       // places the item at the back of the vector directly
       // this may also expand the vector but we have already
       // reserved the space so that shouldn't happen
       data.emplace_back(d);
       ++count;
   }

   for(size_t i = 0; i < NUM_DATUM/2; i++){
       cout << data[i] + data[i + NUM_DATUM/2] << endl;
   }
}

The most powerful feature of c++ is not classes, it is the destructor. The destructor allows for resources or responsibilities to be discharged or released when the scope is exited. This means you don't have to re-write cleanup code multiple times. Moreover because only constructed objects can be destructed; if you never get to an item and thus never construct it, you do not pay any penalty in destruction if something happens.

If you find yourself repeating cleanup code, that should be a flag that the code in question is not taking advantages of the power of the destructor and RAII.

Mgetz
  • 5,108
  • 2
  • 33
  • 51
7

In C, there is a common goto idiom used to handle errors and exit cleaning from a function. I've read that exception handling via try-catch blocks is preferred in object-oriented programs,

That's not true at all for C++.

But C++ has deterministic destructors instead of finally blocks (which are used, for example, in Java), and that's a game changer for error-handling code.

I've read that all this cleanup code in the catch blocks may be unnecessary in C++ due to something called RAII,

Yes, in C++ you use "RAII". Which is a poor name for a great concept. The name is poor because it puts the emphasis on initialisation (Resource Acquisition Is Initialisation). The important thing about RAII, in contrast, lies in destruction. As the destructor of a local object will be executed at the end of a block, no matter what happens, be it early returns or even exceptions, it's the perfect place for code that releases resources.

but I'm unfamiliar with the concept.

Well, for the very beginning, you can start with Wikipedia's definition:

http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

Or you go straight to Bjarne Stroustrup's website:

http://www.stroustrup.com/bs_faq2.html#finally

I'm sure we'd be more than happy to answer questions about particular aspects of the idiom or problems that you encounter using it :)

Is my implementation proper, or is there a better way to handle errors and cleanly exit a function in C++?

Your implementation is not what one would expect from good C++ code.

Here is an example using RAII. It uses exceptions to report errors, and destructors to perform cleanup operations.

#include <fstream>
#include <stdexcept>
#include <vector>

// C or low-level functions to be wrapped:
int blackbox();
void undo_blackbox();

// just to be able to compile this example:
FILE *fp;

// The only self-made RAII class we need for this example
struct Blackbox {
    Blackbox() {
        if (!blackbox()) {
            throw std::runtime_error("blackbox failed");
        }
    }

    // Destructor performs cleanup:
    ~Blackbox() {
        undo_blackbox();
    }   
};

void foobar(void){
    // std::ifstream is an implementation of the RAII idiom,
    // because its destructor closes the file:
    std::ifstream is("blah.txt");
    if (!is) {
        throw std::runtime_error("could not open blah.txt");
    }

    Blackbox local_blackbox;

    // std::vector itself is an implementation of the RAII idiom,
    // because its destructor frees any allocated data:
    std::vector<unsigned long> data(42);

    for(size_t i = 0; i < data.size(); i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            throw std::runtime_error("fgets error");
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < (data.size()/2); i++){
        printf("%lu\n", data[i] + data[i + (data.size()/2)]);
    }

    // nothing to do here - the destructors do all the work!
}

By the way, +1 for trying to learn a new concept in a new language. It's not easy to change your mindset in a different language! :)

Christian Hackl
  • 27,051
  • 3
  • 32
  • 62
  • Might be off-topic, but I really liked the emphasis on the lack of `finally` in C++ as compared to Java. Actually it's obvious, but I've never thought about it. +1 for that! – andreee Feb 06 '17 at 13:32