Initializations in if

One of the capabilities added in C++17 was using initializations in if/else statements:

if (auto foo = f(); foo.isValid())
...

The primary use of this is to restrict the scope of the variables (which can include scoping variables like lock guards).

However, it also provides for some slightly neater and better self-documenting code.

There's a fundamental difference between

Foo f = func();
if (!f.isNull())
{
    doSomething(f);
}
else
{
    f = func2();
    doSomethingElse(f);
}

//Use f more generally

And the same block where f is not used again: the example above is a creation pattern with side effects.   It could be rewritten as

std::pair<Foo, bool> MakeFoo() {
if (Foo f = func(); !f.isNull())
{
    return std::make_pair(f, true);
}
else
{
    return std::make_pair(func2(), false);
}
}
...
auto [f, flag] = MakeFoo();
if (flag)
    doSomething(f);
else
    doSomethingElse(f);
...

where the creation logic is separated from the side effects.

In contrast

if (Foo f = func(); !f.isNull())
    doSomething(f);
else
    doSomethingElse(func2());

is not a creation pattern. You can do the same refactoring, but it makes the logic slightly more, not less, complex, to extract the creation logic.

if (auto [f, flag] = MakeFoo(); flag)
    doSomething(f);
else
    doSomethingElse(f);

Another effect of appropriate use is simplifying nesting levels.  Consider:

if (x.isBinary())
   process(x);
else
{
    auto y = z.find(key);
    if (y == z.end())
         process1(x);
    else
         process2(x, y);
}

We have doubly nested conditions. By using the new feature, we can flatten the tests:

if (x.isBinary())
    process(x);
else if (auto y = z.find(key); y == z.end())
    process1(x);
else
    process2(x, y);

In this case the effective scope of y is not actually narrowed much - it was already scoped by the braces in the else condition - but the layout has been clarified.

One particular use case should really become an idiom:

auto iter = std::find_if(coll.begin(), coll.end(), func);
if (iter != coll.end())
{
    // dereference and use iter
}

should probably always be

if (auto iter = std::find_if(coll.begin(), coll.end(), func); iter != coll.end())
{
    // dereference and use iter
}

unless there is a sentinel or other characteristic of the data which means that the test can be dispensed with altogether. (This applies to find, find_if and the various container/string member find functions.)  This allows for a stylistic model of using the same symbol (here "iter") for a very temporary iterator in much the same way as i, j, and k can be used by convention as integral loop control variables. (Note that using integral loop control values, in a simple way, should be deprecated after the introduction, in C++20, of the iota_view range.  They may still be needed where you want to iterate over something else but keep a counter.)

These are among the very short list of cases where expressive naming can default to short symbols reflecting a functionality, as long as the convention is clearly understood by everyone working with the code base.  If you are going to do multiple things with an iterator outside of a if/else block then allowing it to escape into a more general processing context without giving it a more expressive name would be frowned upon.

Thus, though we can use:

if (auto iter = std::find_if(coll.begin(), coll.end(), func); iter != coll.end())
{
    // dereference and use iter
}
else
{
    // Do something by default
}

if we do have a sentinel or a data model guaranteeing a successful match, we should use:

auto widget_iter = std::find_if(coll.begin(), coll.end(), func);

// Process widget_iter, possibly in rather complex ways.

Or, possibly:

const auto & widget_ref = *(std::find_if(coll.begin(), coll.end(), func));

(or:

Widget widgetForProcessing(*(std::find_if(coll.begin(), coll.end(), func)));

if we want to make a copy / initialize a new object with the found value), which elides the use of the iterator by dereferencing it immediately.

If we don't care about the value in the iterator, but only about whether a value was found, then we should avoid using find/find_if, period, which also removes the need for using a variable initialization at all:

if (std::any_of(coll.begin(), coll.end(), func))
{
    // Process when condition met
}
else
{
    // Process when condition not met
}

Comments

Popular posts from this blog

Boundaries

Code Reviews

Overview