Skip to content

Patternia v0.6.3 Release Note

Release Date: January 2, 2026 Version: 0.6.3


🎆Happy NewYear!🎆

Overview

Patternia v0.6.3 introduces a performance-focused refactor that removes the lvalue-qualified when() method overload. This change enforces rvalue usage of the match builder, eliminating unnecessary tuple copying overhead during case accumulation and improving both runtime performance and code clarity.


API Changes

Removed Lvalue-Qualified when() Overload

Removed: The const & qualified overload for the when() method has been removed to enforce rvalue-only usage of the match builder.

Previous Behavior (v0.6.2):

// Two overloads existed - rvalue and lvalue
template <typename CaseExpr>
constexpr auto when(CaseExpr &&expr) && { /* rvalue-qualified */ }

template <typename CaseExpr>
constexpr auto when(CaseExpr &&expr) const & { /* lvalue-qualified - REMOVED */ }

New Behavior (v0.6.3):

// Only rvalue-qualified overload remains
template <typename CaseExpr>
constexpr auto when(CaseExpr &&expr) && {
    static_assert(!has_pattern_fallback,
        "[Patternia.match]: no cases may follow a wildcard ('__') pattern");

    static_assert(ptn::core::traits::is_case_expr_v<std::decay_t<CaseExpr>>,
        "Argument to .when() must be a case expression created with the '>>' operator");

    static_assert(ptn::core::traits::is_handler_invocable_v<std::decay_t<CaseExpr>, subject_type>,
        "Handler signature does not match the pattern's binding result");

    using case_t = std::decay_t<CaseExpr>;
    using new_cases = tuple_utils::tuple_append_t<cases_type, case_t>;

    using builder_t = match_builder<subject_type, has_match_fallback, new_cases>;

    return builder_t{
        std::forward<subject_type>(subject_), std::move(new_cases)};
}


Usage Examples

Correct Usage (Rvalue Chains)

// v0.6.3 - Correct: chained temporary usage
auto result = match(value)
    .when(lit(1) >> "one")
    .when(lit(2) >> "two")
    .otherwise("other");

Incorrect Usage (No Longer Supported)

// v0.6.3 - ERROR: Cannot call when() on lvalue match_builder
auto builder = match(value);
builder.when(lit(1) >> "one");  // COMPILE ERROR in v0.6.3
builder.when(lit(2) >> "two");  // COMPILE ERROR in v0.6.3
auto result = builder.otherwise("other");

// v0.6.2 - Previously allowed (but inefficient)
auto builder = match(value);
builder.when(lit(1) >> "one");  // Allowed but caused tuple copying
builder.when(lit(2) >> "two");  // Allowed but caused tuple copying
auto result = builder.otherwise("other");

Alternative for Lvalue Patterns

For scenarios requiring intermediate storage, use std::move:

// v0.6.3 - Alternative: explicitly move the builder
auto builder = match(value);
builder = std::move(builder).when(lit(1) >> "one");
builder = std::move(builder).when(lit(2) >> "two");
auto result = std::move(builder).otherwise("other");

Implementation Details

Performance Optimization

Problem: The lvalue-qualified when() overload required copying the internal tuple of cases on each invocation, leading to O(n²) tuple copying complexity for n cases.

Solution: By enforcing rvalue-only usage, each when() call can move the tuple instead of copying it, reducing complexity to O(n).

Performance Impact:

// Before (v0.6.2) - Tuple copying at each step
auto builder = match(value);
builder.when(case1);  // Copy empty tuple → tuple<case1>
builder.when(case2);  // Copy tuple<case1> → tuple<case1, case2>
builder.when(case3);  // Copy tuple<case1, case2> → tuple<case1, case2, case3>
// Total: 3 copies of growing tuples

// After (v0.6.3) - Tuple moving at each step
match(value)
    .when(case1)  // Move empty tuple → tuple<case1>
    .when(case2)  // Move tuple<case1> → tuple<case1, case2>
    .when(case3)  // Move tuple<case1, case2> → tuple<case1, case2, case3>
// Total: 0 copies, 3 moves

Code Simplification

Removed Code: 29 lines of duplicate implementation logic were eliminated.

// REMOVED: Lvalue-qualified overload
template <typename CaseExpr>
constexpr auto when(CaseExpr &&expr) const & {
    static_assert(!has_pattern_fallback,
        "[Patternia.match]: no cases may follow a wildcard ('__') pattern");

    static_assert(ptn::core::traits::is_case_expr_v<std::decay_t<CaseExpr>>,
        "Argument to .when() must be a case expression created with the '>>' operator");

    static_assert(ptn::core::traits::is_handler_invocable_v<std::decay_t<CaseExpr>, subject_type>,
        "Handler signature does not match the pattern's binding result");

    using case_t = std::decay_t<CaseExpr>;
    using new_cases = tuple_utils::tuple_append_t<cases_type, case_t>;

    using builder_t = match_builder<subject_type, has_match_fallback, new_cases>;

    return builder_t{
        subject_, std::tuple_cat(cases_, std::make_tuple(expr))};  // Tuple CAT with copy
}

The removed overload used std::tuple_cat(cases_, std::make_tuple(expr)), which created a new tuple by copying the existing cases_ tuple, while the retained rvalue version uses std::move(new_cases) for efficient move semantics.


Migration Guide

From v0.6.2 to v0.6.3

1. Refactor Lvalue Builder Usage:

// v0.6.2 - Lvalue usage (no longer supported)
auto builder = match(value);
builder.when(pattern1 >> handler1);
builder.when(pattern2 >> handler2);
auto result = builder.otherwise(default_handler);

// v0.6.3 - Migrate to rvalue chain
auto result = match(value)
    .when(pattern1 >> handler1)
    .when(pattern2 >> handler2)
    .otherwise(default_handler);

2. Conditional Case Addition:

// v0.6.2 - Conditional cases with lvalue
auto builder = match(value);
if (condition1) {
    builder.when(pattern1 >> handler1);
}
if (condition2) {
    builder.when(pattern2 >> handler2);
}
auto result = builder.otherwise(default_handler);

// v0.6.3 - Alternative approaches

// Option A: Use std::move in each branch
auto builder = match(value);
if (condition1) {
    builder = std::move(builder).when(pattern1 >> handler1);
}
if (condition2) {
    builder = std::move(builder).when(pattern2 >> handler2);
}
auto result = std::move(builder).otherwise(default_handler);

// Option B: Build cases dynamically (preferred for complex conditions)
auto result = match(value, build_cases(condition1, condition2)).end();

3. Debugging/Logging:

// v0.6.2 - Lvalue for intermediate inspection
auto builder = match(value);
builder.when(pattern1 >> handler1);
// Inspect builder state...
builder.when(pattern2 >> handler2);

// v0.6.3 - Use explicit moves if needed
auto builder = match(value);
builder = std::move(builder).when(pattern1 >> handler1);
// Inspect builder state...
builder = std::move(builder).when(pattern2 >> handler2);

Breaking Changes

API Changes

  • Lvalue-Qualified when() Removed: Calling when() on an lvalue match_builder now results in a compile-time error.

Impact Analysis

  • Low Impact: Most existing code already uses the idiomatic rvalue chain pattern
  • Compile-Time Detection: Errors are caught at compile time, not runtime
  • Simple Migration: Affected patterns can be refactored with straightforward syntax changes
  • Performance Benefit: The change improves performance for all patterns, even unaffected ones

Code Patterns Affected

Affected Pattern:

auto builder = match(value);
builder.when(case1);
builder.when(case2);

Unaffected Pattern:

auto result = match(value)
    .when(case1)
    .when(case2);


Documentation Improvements

This release also includes documentation updates to: - Clarify the rvalue-only usage pattern for match builders - Add examples of proper match builder chaining - Update migration guidance for affected code patterns - Enhance performance optimization documentation


Note: This release focuses on internal optimization and API refinement. The core pattern matching functionality and runtime behavior remain unchanged from v0.6.2. All existing patterns using the idiomatic rvalue chain pattern continue to work without modification.