2

Let's say I'm doing something that requires cleanup when an exception is thrown.

For example, say I'm creating a dynamic array, and I need to construct objects, but their constructors may throw an exception:

size_t const n = 100;
T *const p = static_cast<T *>(operator new(sizeof(T) * n));
size_t i;
for (i = 0; i < n; ++i)
    new (&p[i]) T(1, 2, 3);      // Not exception-safe if T::T(T const &) throws!

I can fix it either via catch (...) { ...; throw; }:

size_t const n = 100;
T *const p = static_cast<T *>(operator new(sizeof(T) * n));
size_t i;
try
{
    for (i = 0; i < n; ++i)
        new (&p[i]) T(1, 2, 3);
}
catch (...)
{
    while (i > 0)
        p[--i].~T();
    operator delete(p);
    throw;
}

or via a scoped destructor:

size_t n = 100;
struct Guard
{
    T *p;
    size_t i;
    Guard(size_t n) : i(), p(static_cast<T *>(operator new(sizeof(T) * n))) { }
    ~Guard()
    {
        while (i > 0)
            p[--i].~T();
        operator delete(p);
    }
} guard(n);

for (guard.i = 0; guard.i < n; ++guard.i)
    new (&guard.p[guard.i]) T(1, 2, 3);

guard.i = 0;     // Successful... "commit" the changes
guard.p = NULL;  // or whatever is necessary to commit the changes

Which technique should I prefer to use when, and why?

(Note: This example is only meant to show the difference between two techniques. I know it's not perfect code, so please do not focus on this particular example. It's just for illustration.)

user541686
  • 205,094
  • 128
  • 528
  • 886

2 Answers2

2

The solution with destructor is better than the explicit try/catch:

  • it is reusable, if you will need to do similar initialisation in another function, you can just reuse the same guard class
  • it is easier to maintain - let's say that in some situation your function needs to return with failure but no exception is thrown. With the guard class it is handled more or less automatically
  • it is cleaner, since the code is more modular
Wojtek Surowka
  • 20,535
  • 4
  • 44
  • 51
  • I guess that begs the question -- when are you supposed to use `catch (...) { ...; throw; }` then? – user541686 Mar 12 '14 at 10:11
  • I cannot easily imagine a situation in which it is better than a guard class, so the answer would be - never. C++ has destructors for something, and the `try/catch` construct is more for languages without destructors. – Wojtek Surowka Mar 12 '14 at 10:13
  • +1 I see. I guess that means `catch (...)` is entirely useless right? (I'm saying that because catch-all without rethrow is already known to be bad practice, so if `catch (...) { ...; throw; }` is also not preferred then it means `catch (...)` shouldn't be used at all.) – user541686 Mar 12 '14 at 10:25
  • I wouldn't say it is useless entirely, but only that it should be avoided. It may be useful e.g. to log an exception for debugging purposes and then rethrow it. – Wojtek Surowka Mar 12 '14 at 10:28
  • Hmm I'm not sure how it would be useful even in that scenario. Couldn't you do that with a destructor too? – user541686 Mar 12 '14 at 10:48
  • No because in order to log an exception I need access to it, which I do not have in the destructor. – Wojtek Surowka Mar 12 '14 at 10:59
  • You don't have access to it here either though. Remember we're talking about `catch (...)`, not `catch (std::exception &ex)`... – user541686 Mar 12 '14 at 11:15
  • The access is provided with `std::current_exception()`. – Wojtek Surowka Mar 12 '14 at 11:27
  • Oh I see, but that's C++11. What about before then, like in C++03? Was it useless then? – user541686 Mar 12 '14 at 11:41
  • Still in 03 you can log in `catch(...)` that an exception was thrown, which may be useful even without logging details about the exception. – Wojtek Surowka Mar 12 '14 at 12:34
  • Sure but then can't you do that with destructors? – user541686 Mar 12 '14 at 23:33
2

In general, I would say it is a matter of scaling and safety.

The problem with try/catch is two-fold:

  • safety issue: any early return that ignores the catch (somehow) fails to cleanup
  • scaling issue: nested try/catch blocks make a mess of the code
  • scoping issue: to be accessible in the catch the variable must be defined before the try and thus supports default-construction/nullability; it can be painful

Instead, Deferred Statements and Guards do not create unnecessary blocks/scopes and thus no indentation, and read linearly.

Example:

char buffer1[sizeof(T)];
try {
    new (buffer1) T(original);

    char buffer2[sizeof(T)];
    try {
        new (buffer2) T(original);

        // stuff here

    } catch(...) {
        reinterpret_cast<T*>(buffer2)->~T();
        throw;
    }

} catch(...) {
    reinterpret_cast<T*>(buffer1)->~T();
    throw;
}

Compared to:

char buffer1[sizeof(T)];
new (buffer1) T(original);
Defer const defer1{[&buffer1]() { reinterpret_cast<T*>(buffer1)->~T(); } };

char buffer2[sizeof(T)];
new (buffer2) T(original);
Defer const defer1{[&buffer2]() { reinterpret_cast<T*>(buffer2)->~T(); } };

// stuff here

I would note that it seems a good idea to generalize those:

class Guard {
public:
    explicit Guard(std::function<void()> f): _function(std::move(f)) {}

    Guard(Guard&&) = delete;
    Guard& operator=(Guard&&) = delete;

    Guard(Guard const&) = delete;
    Guard& operator=(Guard const&) = delete;

    ~Guard() {
        if (not _function) { return; }
        try { _function(); } catch(...) {}
    }

    void cancel() { _function = std::function<void()>{}; }

private:
    std::function<void()> _function;
}; // class Guard

class Defer {
public:
    explicit Defer(std::function<void()> f): _guard(std::move(f)) {}
private:
    Guard _guard;
}; // class Defer
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • Should be `defer1{[&]()...`. What is the point of Defer? It just hides `cancel()`. – BЈовић Mar 12 '14 at 10:20
  • @BЈовић: documentation => there is no possibility, ever, that it can be cancelled; you don't have to scan the code to check for it, you know it's not possible. – Matthieu M. Mar 12 '14 at 10:21
  • Very elegant and general! I also don't get why Defer is necessary. –  Mar 12 '14 at 15:09
  • @GrapschKnutsch: If you know about the Proxy pattern, one of its use is to *restrict the interface*. Well, `Defer` is a `Guard` which cannot be cancelled; the interface is restricted so that when I read `Defer const deferred{...};` I know it will be executed, always, without even checking the rest of the function => the compiler guarantees that it cannot be cancelled. – Matthieu M. Mar 12 '14 at 16:11