5

You probably know situations like this where you just want to assign to a (const) variable with an expression which might fail (throw) (e.g.container.at()) which forces you to write boiler plate code:

void foo(const string &key) {
    auto it = data_store.find(key);
    if (it == data_store.end()) {
        return;
    }
    const auto & element = it->second;
    ...
    go on with `element`...
    ...
}

In Python you could write code like this:

def foo(name):
    try:
        element = data_store[key]
    except KeyError:
        return
    ..
    go on with `element`
    ..

.. with is less noisy because you don't introduce that useless extra it just for checking existence.

If C++'s try would not introduce a variable scope you could just use at():

void foo(const string &key) {
    try {
        const auto & element = data_store.at(key);
    } catch (const out_of_range &) {
        return;
    }
    ...
    go on with `element`...
    ...
}

What's the way to go here if you don't want to abandon constness and keep your code clean?

If lambdas only could have a try/catch body you could write

void foo(const string &key) {
    const auto & element = [&] () -> T try {
        return data_store.at(key);
    } catch () {
        return;
    } ();
    ...
    go on with `element`...
    ...
}

Some answers to similar questions suggest try/catch blocks around all the code:

void foo(const string &key) {
    try {
        const auto & element = data_store.at(key);
        ...
        go on with `element`...
        ...
    } catch (const out_of_range &) {
        return;
    } catch (some other exception) {
        ...
    } catch (some other exception) {
        ...
    }
}

But I don't like this because of three reasons:

  • there's no visual correlation between at() and it's catch block
  • there might be code that also need you to handle out_of_range
  • you have to write nested code

Which (nice, short and clean) alternatives do you know?

frans
  • 8,868
  • 11
  • 58
  • 132
  • Do you really *need* to check for existence? If `data_store` is a `std::map` or `std::unordered_map`, then the act of just accessing an "element" (as in `data_store[key]`) will *create* that element. What is the use-case? What is the actual problem you try to solve? – Some programmer dude Aug 02 '18 at 09:44
  • 1
    In addition to the comment above: Do you really have to catch the exception that might be thrown at this point? What exactly is your exception handler going to do? – Michael Kenzel Aug 02 '18 at 09:45
  • 1
    Exception handling allows *presumptive* code. I.e., you run some statement of code. You've now arrived at the next statement. Because you're *not* now within some catch-block you can *presume* the statement just executed did *not* throw, and therefore, worked. In short, your last example is easily most common (at least where I work). Writing procedural wrappers to catch exceptions for logging and either re-throwing or consuming are usually the deviance to that. Your description seems to be saying in summary: "I want inline try-catch safety without inline try-catch safety. Tall order. – WhozCraig Aug 02 '18 at 09:47
  • My question is about assigning a variable in a `try` block - the `at()` example is just for having some actual code. You could also imagine opening a file or anything like this. Anyway sometimes you *might* want to auto-insert into a `map` but this is not always the case (and a *very* bad idea to let `map` behave like this in my eyes) – frans Aug 02 '18 at 09:57
  • What is the problem with your first version (the one with the `find`)? Why is it boilerplate code? In my eyes, that's the best version. Fastest, cleanest (And even, if you don't break that `if` into 3 lines, then it is the shortest one). Why complicate things with exceptions? – geza Aug 02 '18 at 10:12

1 Answers1

2

There are three good options on this thread, there's not really any other option.

Those cases assume we are initializing an object; for initializing a reference as you are, apply the techniques to std::reference_wrapper, or a pointer.


BTW I would not discount your first code sample so quickly. It's simpler than all the other options, and it is a common recommendation in C++ to only use exceptions for exceptional conditions -- things you do not expect to be a normal part of the function's contract. It's not idiomatic to use them as a shortcut.

In other words, if the function design is to do nothing if the lookup fails, then throw-catching is an unnecessary complication to the function. You've just written an even uglier version of C-style error handling.

The whole point of the at() accessor is that your function can be kept simple by not catching -- the exception can be left to propagate up to a more general error handler.

M.M
  • 138,810
  • 21
  • 208
  • 365