C++ / a working model

96 / 163   ·   C++17   ·   11 min

Fold expressions: empty input, short-circuit, and evaluation order

Keep this sentence

When combining multiple conditions into one validator, first define empty-set and post-failure behavior, then choose the fold operator. C++17’s logical-and fold preserves short-circuit, but cannot exempt an invalid template branch from compilation.

In this lesson
  1. Write the combination rules first, not the ellipsis
  2. Short-circuit happens at runtime; validity checking happens at compile time
  3. Parenthesis direction is not the evaluation order of every operator
  4. Example
  5. Exercise
READING EVIDENCE / Partial text read

C++ Templates: The Complete Guide

Sequentially read the readable main text and textual code of chapters 1–28 and appendices A–E of the second edition, and also cross-read official chapters 4 and 23. Inspected all 12 PDF pages corresponding to the 13 substantive figures: Figures 18.1–18.5 and D.1 are visible and have been read; Figures 13.1, 21.1–21.4, 27.1, and B.1 remain only as titles or links in the obtained PDF, with the graphics missing (7 figures in total), so coverage is still listed as partial and titles are not treated as having read the images. The graphic for Figure 18.4 is on the page before the title and has been verified. The actual edition is the second edition published in 2017 with copyright 2018; this is not presented as having read the first edition. Table of contents, bibliography, and index are not counted as main text. Concepts in the book are a pre-finalization C++20 design; the old-style -> bool syntax must not be treated as final C++20.

Edition, actual reading range, and original sources →

Write the combination rules first, not the ellipsis

Suppose each checker takes an integer and returns true or false; the combiner succeeds only when every check passes. The natural identity here is true: no checkers means no condition was violated. Therefore all_of_checks(7) should return true, rather than requiring an extra empty-parameter overload.

The example’s (... && static_cast<bool>(checks(value))) is a unary left fold. The empty-pack rule for && supplies true, and the explicit conversion ensures the built-in logical and is used, so a custom return type cannot change short-circuit semantics via an overloaded operator. Templates reduce repeated structure, not the need to specify behavioral boundaries.

Short-circuit happens at runtime; validity checking happens at compile time

Built-in && evaluates the left operand first; once it is false, later checkers are not called. The second set of assertions makes the first check fail for sure and records whether the second ran, proving that “all conditions” was not mistakenly written as bitwise & which always evaluates. Checker objects are already constructed before entry; short-circuit only constrains calls inside the function body and cannot undo side effects of constructing arguments at the call site.

Even if the first check always returns false, every checks(value) must still be a valid expression. If some type cannot be called with an integer, instantiation fails; this is not compile-time branch discarding as with if constexpr. Elements of the parameter pack may have different types, but they must all satisfy the interface the combiner actually uses.

Parenthesis direction is not the evaluation order of every operator

The book uses fold expressions to reduce recursion; this lesson further treats them as a small algebraic contract. For &&, left and right folds usually agree on true/false results; for subtraction, (10-3)-2 and 10-(3-2) differ. Do not infer from “left fold” that every operator executes side effects left to right; order is still determined by the chosen operator’s rules.

Checkers are borrowed by const reference; the function does not store them, and temporary lambdas remain valid for the duration of the complete call. Perfect forwarding is not used here because consuming a stateful rvalue multiple times is not an interface requirement. Parameter packs suit heterogeneous checkers known at compile time; if the number of checkers comes from a config file, use a runtime container rather than forcing a dynamic problem into templates.

Pitfalls

  • Runtime && short-circuit does not waive the compile-time validity requirement of every expanded expression.
  • The associativity direction of a fold’s parentheses does not mean every operator is guaranteed the same evaluation order.

Run an example

Minimum C++17 · complete program · Download .cpp

#include <cassert>

template<class... Checks>
bool all_of_checks(const int& value, const Checks&... checks) {
    return (... && static_cast<bool>(checks(value)));
}

int main() {
    assert(all_of_checks(7));
    const auto positive = [](int n) { return n > 0; };
    const auto even = [](int n) { return n % 2 == 0; };
    assert(all_of_checks(8, positive, even));
    assert(!all_of_checks(3, positive, even));

    int calls = 0;
    const auto reject = [&calls](int) { ++calls; return false; };
    const auto expensive = [&calls](int) { calls += 100; return true; };
    const bool accepted = all_of_checks(8, reject, expensive);
    assert(!accepted);
    assert(calls == 1);
}

Compile locally

g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread books-cpp-templates.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Change the combiner to “at least one passes.” What should an empty parameter pack return?

Show a reference answer

Change it to (... || static_cast<bool>(checks(value))). The empty-pack result of built-in || is false, because no check can provide evidence of success; the first true prevents later calls. Change the side-effect assertion so the first check returns true and later checks do not run, and keep the assertion that the result is false when there are no checkers.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog