Customization points

ADL two-step, CPOs, tag_invoke, the member pivot.

Drill points
  • ADL two-step mechanics
  • Why CPOs fix the using std::swap ritual
  • tag_invoke: one ADL name to rule them all
  • C++26 execution's pivot back to member functions

The ADL two-step is the classic protocol: bring the std fallback into scope, call unqualified, let ADL find the user's overload.

template <class T>
void reset(T& a, T& b) {
  using std::swap;      // fallback
  swap(a, b);           // unqualified: ADL may find a better one
}

No change; std::begin/std::end free functions follow the same pattern for ranges.

std::invoke unifies “call anything callable”, which generic code adopts as its dispatch primitive. The two-step remains the customization protocol.

Ranges ships customization point objects: std::ranges::swap, ranges::begin are callable objects that do the two-step internally and enforce constraints. Hidden friends become the preferred hook to customize.

std::ranges::swap(a, b);   // CPO: no using-declaration ritual, concept-checked
class Buf {
  friend auto begin(Buf& b) { return b.data_; }   // hidden friend hook
};

tag_invoke style (one ADL name, dispatch on a tag object) peaks in the senders/receivers proposals and libraries like libunifex — maximal protection from name collisions, at a real readability cost.

The committee pivots: standardized std::execution drops tag_invoke for plain member functions (connect, start, queries) — easier to teach, debug and tool. Lesson: member-first, CPO at the boundary, tag_invoke only with strong reasons.

// C++26 execution customization = write members:
struct my_sender {
  template <class Rcvr>
  auto connect(Rcvr r) { return my_opstate<Rcvr>{std::move(r)}; }
};
C++29direction · not adopted

Direction: a language-level customization mechanism (the P2279 problem statement) and reflection-assisted interface checking remain open Evolution work.

C++26 per the final working draft; C++29 rows are proposals and direction, not adopted features.