Skip to content

Latest commit

 

History

History
245 lines (184 loc) · 12.1 KB

stdinvoke.md

File metadata and controls

245 lines (184 loc) · 12.1 KB

std::invoke


Executive Abstract

std::invoke is a universal, constexpr execution function template in <functional> that invokes any Callable object—including free functions, function pointers, class functors, lambdas, member function pointers (Class::*), and member data pointers—using a single, unified calling interface. Introduced in C++17 and expanded in C++23 with explicit return-type casting (std::invoke_r), std::invoke eliminates syntactic fragmentation across C++ callable categories. It serves as the foundational execution primitive powering generic libraries (std::apply, std::async, std::thread, std::function), C++20 Ranges projections, and the formal C++ INVOKE Protocol Specification.


Motivation & Historical Context

Pre-C++17 Deficiencies: Syntactic Fragmentation of Callables

In C++11 and C++14, invoking different Callable types required completely divergent, mutually incompatible syntax forms:

// Pre-C++17 Incompatible Invocation Syntaxes
auto free_fn = [](int x) { return x * 2; };
free_fn(42); // 1. Direct call syntax: f(args)

struct Widget { int value = 10; int calc(int x) const { return value + x; } };
Widget w;
int (Widget::*mfp)(int) const = &Widget::calc;
(w.*mfp)(42); // 2. Member function pointer requiring object instance: (obj.*mfp)(args)

int Widget::*mdp = &Widget::value;
w.*mdp; // 3. Member data pointer access syntax: obj.*mdp

Deficiencies of Pre-C++17 Callable Execution:

  1. Generic Template Code Duplication: Writing generic higher-order functions (such as thread pools, task queue wrappers, or event callbacks) required writing complex SFINAE overloads or std::bind wrappers to handle free functions vs. member function pointers vs. member data pointers separately.
  2. Indirection & Reference Handling Inconsistencies: Invoking a member pointer required checking whether the target object was a value object (w.*mfp), a raw pointer (ptr->*mfp), or a reference wrapper (ref.get().*mfp).

Solution: ISO C++17 introduced std::invoke(f, args...), standardizing the full C++ INVOKE protocol under a single constexpr function template signature.


Technical Mechanics & The INVOKE Protocol

The Seven Branches of the C++ INVOKE Protocol ([func.require])

When std::invoke(f, t1, t2, ..., tN) is called, the compiler selects one of seven operational branches:

                            std::invoke(f, t1, t2, ..., tN)
                                           │
         ┌─────────────────────────────────┼─────────────────────────────────┐
         ▼                                 ▼                                 ▼
1. Member Function Pointer        2. Member Data Pointer           3. Standard Callable
(f is Class::* member method)    (f is Class::* member field)     (Free function, Lambda, Functor)
         │                                 │                                 │
  (t1.*f)(t2, ..., tN)                   t1.*f                         f(t1, t2, ..., tN)
  or (t1.get().*f)(...)            or t1.get().*f
  or ((*t1).*f)(...)               or (*t1).*f
  1. Member Function Pointer + Object Reference: If $f$ is a member function pointer of class $C$, and $t_1$ is an instance of $C$ (or derived): (t1.*f)(t2, ..., tN).
  2. Member Function Pointer + reference_wrapper: If $t_1$ is a std::reference_wrapper<C>: (t1.get().*f)(t2, ..., tN).
  3. Member Function Pointer + Pointer / Smart Pointer: If $t_1$ is a raw or smart pointer (C*, std::unique_ptr): ((*t1).*f)(t2, ..., tN).
  4. Member Data Pointer + Object Reference: If $f$ is a member data pointer, returns reference to field: t1.*f.
  5. Member Data Pointer + reference_wrapper: Returns field reference: t1.get().*f.
  6. Member Data Pointer + Pointer / Smart Pointer: Returns field reference: (*t1).*f.
  7. Standard Function / Functor / Lambda: In all other cases: f(t1, t2, ..., tN).

C++23 Extension: std::invoke_r<R>(f, args...)

C++23 introduced std::invoke_r<R> (proposal P1030R6). It executes std::invoke(f, args...) and implicitly converts the returned result to the target return type $R$:

// C++23 std::invoke_r: Converts return result implicitly to double (or discards if R = void)
double result = std::invoke_r<double>([](int a, int b) { return a + b; }, 10, 20);

Evolution Across Standards (C++17 to C++26/C++29)

┌──────────────────────────────────────────────────────────────────┐
│ C++17                                                            │
├──────────────────────────────────────────────────────────────────┤
│ • Introduced std::invoke in <functional>                         │
│ • Defined formal INVOKE protocol across all callable types       │
│ • Added std::is_invocable and std::invoke_result_t traits        │
└──────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ C++20                                                            │
├──────────────────────────────────────────────────────────────────┤
│ • std::invoke made fully constexpr-capable                       │
│ • Introduced C++20 Concepts: std::invocable<F, Args...>          │
│ • Underpins C++20 Ranges projection functions                    │
└──────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────────────┐
│ C++23 / C++26                                                    │
├──────────────────────────────────────────────────────────────────┤
│ • Introduced std::invoke_r<R>(f, args...) in C++23               │
│ • Powers modern zero-allocation std::function_ref (C++26)        │
└──────────────────────────────────────────────────────────────────┘

Comprehensive Code Examples

Example 1: Universal Execution Across Free Functions, Member Methods, and Data Pointers

Demonstrates a single function template executing heterogeneous callables using std::invoke.

#include <iostream>
#include <functional>
#include <string>

// 1. Free Function
int add_free(int a, int b) { return a + b; }

// 2. Struct with Member Function and Member Data Pointer
struct Calculator {
    int factor = 10;
    int multiply(int value) const { return value * factor; }
};

int main() {
    // 1. Invoke Free Function
    int res1 = std::invoke(add_free, 20, 22);
    std::cout << "Free Function: " << res1 << " (Expected: 42)\n";

    // 2. Invoke Lambda Closure
    auto lambda_op = [](int a, int b) { return a - b; };
    int res2 = std::invoke(lambda_op, 50, 8);
    std::cout << "Lambda:        " << res2 << " (Expected: 42)\n";

    Calculator calc;

    // 3. Invoke Member Function Pointer via Object Reference
    int res3 = std::invoke(&Calculator::multiply, calc, 4);
    std::cout << "Member Method: " << res3 << " (Expected: 40)\n";

    // 4. Invoke Member Function Pointer via Object Pointer
    int res4 = std::invoke(&Calculator::multiply, &calc, 5);
    std::cout << "Member Pointer: " << res4 << " (Expected: 50)\n";

    // 5. Invoke Member Data Pointer (Reads 'factor' field directly!)
    int factor_val = std::invoke(&Calculator::factor, calc);
    std::cout << "Member Field:  " << factor_val << " (Expected: 10)\n";

    return 0;
}

Example 2: C++20 Concepts (std::invocable) & std::invoke_result_t

Demonstrates inspecting return types and constraining generic functions using C++20 concepts.

#include <iostream>
#include <functional>
#include <type_traits>
#include <concepts>

// C++20 Function constrained by std::invocable concept
template <typename F, typename... Args>
requires std::invocable<F, Args...>
constexpr auto execute_and_log(F&& f, Args&&... args) {
    std::cout << "Executing constrained callable...\n";
    return std::invoke(std::forward<F>(f), std::forward<Args>(args)...);
}

int main() {
    auto sq = [](int x) constexpr { return x * x; };

    // Inspect return type at compile time via std::invoke_result_t
    using RetType = std::invoke_result_t<decltype(sq), int>;
    static_assert(std::is_same_v<RetType, int>);

    constexpr int result = execute_and_log(sq, 7);
    std::cout << "Result: " << result << " (Expected: 49)\n";

    return 0;
}

Example 3: Modern C++23 Explicit Return Casting (std::invoke_r)

Demonstrates C++23 std::invoke_r casting returned values to target types implicitly or discarding them cleanly.

#include <iostream>
#include <functional>

int compute_integer() { return 42; }

int main() {
#if defined(__cpp_lib_invoke_r) && __cpp_lib_invoke_r >= 202106L
    // C++23 std::invoke_r: Executes function and converts 'int' return to 'double'!
    double d_val = std::invoke_r<double>(compute_integer);

    // C++23 std::invoke_r with 'void': Discards return value safely!
    std::invoke_r<void>(compute_integer);

    std::cout << "Converted double result: " << d_val << "\n";
#else
    double d_val = static_cast<double>(std::invoke(compute_integer));
    std::cout << "Converted double result: " << d_val << "\n";
#endif

    return 0;
}

Engineering Best Practices & Common Pitfalls

  1. Always Use std::invoke inside Generic Wrapper Code:

    • Inside generic function templates, task queues, or logging wrappers, write std::invoke(f, std::forward<Args>(args)...) instead of direct function calls f(args...). This ensures your wrapper accepts member pointers (Class::*) and references seamlessly.
  2. Member Data Pointer Argument Requirement:

    • When passing a member data pointer (&Class::field) to std::invoke, you MUST provide an instance of Class (or pointer/reference wrapper) as the second parameter (std::invoke(&Class::field, object_instance)).
  3. Use C++20 std::invocable Concepts:

    • Replace complex SFINAE traits (std::enable_if_t<std::is_invocable_v<F, Args...>>) with clean C++20 concept constraints (requires std::invocable<F, Args...>).

Relationships

  • Related to:
    • std::apply — Unpacks tuples into std::invoke arguments.
    • std::function — Polymorphic wrapper using std::invoke internally.
    • std::invoke_r (C++23) — Return-type converting variant.
    • Concepts std::invocable (C++20) — Type constraints checking std::invoke validity.
  • Superseded by: None.
  • Supersedes: Fragmented manual invocation branching for free functions vs. member pointers.

Back to C++17 section.