C++23 Explicit Object Parameter for Method Chaining
The introduction of the explicit object parameter in C++23, colloquially known as "deducing this," fundamentally simplifies member function chaining by eliminating the boilerplate traditionally required for fluent interfaces. This feature allows member functions to explicitly declare the object instance as their first parameter, enabling the compiler to deduce the object's exact type, constness, and value category. As a result, developers can write single member functions that seamlessly maintain method chains across inheritance hierarchies and diverse value categories without resorting to the Curiously Recurring Template Pattern (CRTP) or duplicating overloads.
The Problem with Traditional Method Chaining
In standard member function chaining, a method returns a reference to
*this (return *this;). While straightforward
in simple classes, this approach breaks down in two primary
scenarios:
- Inheritance Hierarchies: If a base class method
returns
Base&, calling it on a derived class instance casts the chain back toBase&. Any subsequent calls to methods exclusive to the derived class will fail to compile. To fix this prior to C++23, developers had to use CRTP or manually override and cast methods in every derived class. - Value Category Handling: If a builder pattern needs
to support both lvalues and rvalues (to allow move semantics when
building temporary objects), developers had to duplicate functions with
ref-qualifiers: one for
&and one for&&.
How Explicit Object Parameters Solve the Issue
C++23 allows member functions to declare the implicit
this pointer as an explicit parameter preceded by the
this keyword. By making this parameter a forwarding
reference, the function deduces both the precise derived type and the
value category (lvalue or rvalue).
class BuilderBase {
public:
template <typename Self>
auto&& set_id(this Self&& self, int id) {
// assign id...
return std::forward<Self>(self);
}
};
class ConcreteBuilder : public BuilderBase {
public:
template <typename Self>
auto&& set_name(this Self&& self, std::string name) {
// assign name...
return std::forward<Self>(self);
}
};Solving Derived Class Slicing
When an explicit object parameter is templated
(this Self&& self), Self resolves to
the type through which the function was actually called.
ConcreteBuilder builder;
builder.set_id(42) // Resolves Self as ConcreteBuilder&, returns ConcreteBuilder&
.set_name("Item"); // Valid: set_name is accessible because the chain was not sliced to BuilderBase&Because set_id deduces self as
ConcreteBuilder&, it returns a reference to
ConcreteBuilder, keeping the derived context intact
throughout the chain. This replaces CRTP entirely for this use case,
yielding cleaner definitions and faster compile times.
Unifying Lvalue and Rvalue Chaining
The same mechanism handles temporary objects automatically. If the chain is invoked on an rvalue:
auto item = ConcreteBuilder{}
.set_id(1) // Self deduced as ConcreteBuilder&&
.set_name("Temp"); // Self deduced as ConcreteBuilder&&std::forward<Self>(self) preserves the rvalue
category throughout each step of the pipeline. In pre-C++23 code,
handling both lvalue and rvalue method chaining required two distinct
overloads per chained method. With explicit object parameters, a single
implementation handles lvalues, const lvalues, and rvalues
correctly.