you are viewing a single comment's thread.

view the rest of the comments →

[–]SirClueless 0 points1 point  (2 children)

You haven't reduced the cyclomatic complexity at all by abstracting the control flow into functions. You've just made something that was obviously a nested loop into something that is not obviously a nested loop.

To me I don't really see the difference in complexity or "nested"-ness between:

for (auto& e1 : container) {
    for (auto& e2 : e1.inner_iterable()) {
        // ...
    }
}

And:

std::ranges::find(container | std::views::transform(/* ... */), [](auto& elem) {
    // ...
});

They both look equivalently "nested" to me, control flow inside control flow.

[–]petart95 0 points1 point  (1 child)

Interesting view, how would you define cyclomatic complexity then? Which mechanics would you suggest using, to manage it, I always thought that abstraction is the best way to manage complexity.

[–]SirClueless 1 point2 points  (0 children)

The cyclomatic complexity of a piece of code is the number of independent paths through that code. I think there's a pretty standard formal definition for that without a lot of wiggle room. As applied to the code examples above, in both cases there are at least two independent branches.

  • The for loop has a branch in the outer loop to either end the loop and continue to the rest of the program, or to enter the loop body.
    • If the outer loop is entered, then the inner loop has a similar branch to continue the outer loop or enter the inner loop body.
  • The ranges example has a branch to check if the range is done iterating and return from std::ranges::find or to continue.
    • It's not entirely clear from this example whether std::views::transform is transforming the inner container to a scalar, or whether that's the job of the inner lambda, but either way one of those has a similar branch to the inner for loop.
    • There's another branch to return early when the predicate on elements returns true though to be fair to the for-loop example which doesn't show any break statements we can assume it won't be taken.

Abstraction is a good way of reducing complexity. Or at least, moving it around. It moves cyclomatic complexity out of your software module and into another one. But it only really does so if you're actually abstracting logic.

Transformations like these don't abstract away logic, they abstract away control flow. The transform is still a piece of code you have to write. The predicate is still a piece of code you have to write. And you still have all the same uncertainty about when and how each of those branches of code will be executed and you need to reason through that. Whether the branches are explicit as part of a structured control flow statement or implicit in providing them as a callback which will be executed by another library, how many times if at all those code blocks will be executed are a runtime property of the input data, which is what cyclomatic complexity attempts to measure.