C++ Module 7 – Classes, objects and RAII
Deducing this: explicit object parameters in C++23
Write C++23 member functions that take their object as an explicit this parameter: one function for every kind of object, recursive lambdas and less CRTP.
What you will learn
- Write member functions with an explicit object parameter
- Replace duplicated const, non-const and rvalue overloads with one function template
- Write recursive lambdas without std::function
- Compare an explicit object parameter with the curiously recurring template pattern
Before you start
What you’ll learn · Before you start
What you will learn
- Write member functions with an explicit object parameter
- Replace duplicated const, non-const and rvalue overloads with one function template
- Write recursive lambdas without std::function
- Compare an explicit object parameter with the curiously recurring template pattern
Before you start
On this page
Deducing this is the informal name of a C++23 feature: a member function may declare the object it is called on
as an explicit first parameter, marked with the keyword this, as in int get(this const Counter& self). Inside
the function you write self.n where you used to write n or this->n, and callers still write c.get(). Because
the object is now an ordinary parameter, its type can be deduced like any template parameter. One function template,
auto&& value(this Self&& self), then serves const and non-const objects, lvalues and rvalues, which used to
take four overloads. The same trick lets a lambda call itself, and lets a base class see the derived class it is
called on without the curiously recurring template pattern (CRTP).
This lesson writes explicit object parameters by hand, collapses four overloads into one, writes a recursive depth-first search as a lambda and replaces a CRTP base. GCC supports the feature from version 14 and Clang from version 18 (paper P0847R7); every example runs with Apple clang 21 and GCC 16.
The object becomes a named parameter
The explicit object parameter is the first parameter, written with this in front of it. Its name is up to you,
and self is the usual choice. Its type says how the function receives the object, so the const that used to
follow the parameter list moves into the parameter:
// The object as an explicit first parameter, marked with this (C++23).
#include <print>
struct Counter {
int n = 0;
void bump(this Counter& self) { ++self.n; } // the object is self, not an implicit this
int get(this const Counter& self) { return self.n; } // const goes on the parameter's type
Counter doubled(this Counter self) { // by value: works on a copy
self.n *= 2;
return self;
}
};
int main() {
Counter c;
c.bump(); // called exactly as before
c.bump();
std::println("c.get() = {}", c.get());
Counter d = c.doubled();
std::println("d.get() = {}, c.get() = {}", d.get(), c.get());
// The address of such a member is an ordinary function pointer: the object is just its first argument.
int (*read)(const Counter&) = &Counter::get;
std::println("read(d) = {}", read(d));
std::println("__cpp_explicit_this_parameter = {}", __cpp_explicit_this_parameter);
} from session import run
# explicit_basics.cpp built and run with Apple clang, then with GCC 16.
run("""
clang++ -std=c++23 -Wall -Wextra explicit_basics.cpp -o explicit_basics && ./explicit_basics
g++-16 -std=c++23 -Wall -Wextra explicit_basics.cpp -o explicit_basics-gcc && ./explicit_basics-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra explicit_basics.cpp -o explicit_basics && ./explicit_basics c.get() = 2 d.get() = 4, c.get() = 2 read(d) = 4 __cpp_explicit_this_parameter = 202110 $ g++-16 -std=c++23 -Wall -Wextra explicit_basics.cpp -o explicit_basics-gcc && ./explicit_basics-gcc c.get() = 2 d.get() = 4, c.get() = 2 read(d) = 4 __cpp_explicit_this_parameter = 202110
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 basics_session.py
Three kinds of parameter appear in Counter:
this Counter& selfreceives the object by reference, like an ordinary member function, sobump()changesc.this const Counter& selfmakesget()a read-only function, as aconstmember function is, and it can be called on aconst Counter.this Counter selfreceives a copy.doubled()changes and returns that copy, sodholds 4 whileckeeps 2. Taking a small object by value suits functions that compute a modified version of it.
The calls do not change: c.bump() passes c as self. The address of such a function, &Counter::get, is an
ordinary pointer to a function that takes a const Counter&, called as read(d), because the object really is just
the first argument. The last line prints the feature-test macro, 202110, which code can check with #if before
using the feature.
Inside such a function there is no implicit this, so a member name on its own means nothing:
// In a function with an explicit object parameter, member names need self: there is no implicit this.
struct Counter {
int n = 0;
int get(this const Counter& self) { return n; } // error: n is not self.n here
};
int main() { return Counter{}.get(); } from session import run
# What each compiler says about no_implicit_this.cpp.
run("""
clang++ -std=c++23 -fsyntax-only no_implicit_this.cpp
g++-16 -std=c++23 -fsyntax-only no_implicit_this.cpp
""") Output
$ clang++ -std=c++23 -fsyntax-only no_implicit_this.cpp
no_implicit_this.cpp:4:48: error: invalid use of member 'n' in explicit object member function
4 | int get(this const Counter& self) { return n; } // error: n is not self.n here
| ^
| self.
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -fsyntax-only no_implicit_this.cpp
no_implicit_this.cpp: In explicit object member function 'int Counter::get(this const Counter&)':
no_implicit_this.cpp:4:48: error: invalid use of non-static data member 'Counter::n'
4 | int get(this const Counter& self) { return n; } // error: n is not self.n here
| ^
no_implicit_this.cpp:3:9: note: declared here
3 | int n = 0;
| ^
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 no_this_session.py
Clang even offers the fix, self.. The standard adds a few more rules: a function with an explicit object
parameter cannot be static or virtual, and it cannot have const or a reference qualifier such as && after its
parameter list, since the parameter’s type already says all of that.
One function instead of four overloads
A wrapper that hands out its contents needs a different result for each kind of object. A non-const box gives a
reference that callers can write through, a const box a reference to const, and a box that is about to disappear,
an rvalue, should let its contents be moved out rather than copied. Before C++23 that took four overloads with the
same body. With an explicit object parameter it takes one template:
// One value() for const and non-const, lvalue and rvalue objects, instead of four overloads.
#include <print>
#include <string>
#include <type_traits>
#include <utility>
template <class T>
class Box {
public:
explicit Box(T v) : value_(std::move(v)) {}
// Before C++23 this took four overloads:
// T& value() &; const T& value() const&; T&& value() &&; const T&& value() const&&;
// Self is deduced from the object, and std::forward_like gives value_ the same constness and value category.
template <class Self>
auto&& value(this Self&& self) {
return std::forward_like<Self>(self.value_);
}
private:
T value_;
};
// Names the kind of reference an expression of type R is.
template <class R>
const char* kind() {
using V = std::remove_reference_t<R>;
if constexpr (std::is_lvalue_reference_v<R>) return std::is_const_v<V> ? "const std::string&" : "std::string&";
else return std::is_const_v<V> ? "const std::string&&" : "std::string&&";
}
int main() {
Box<std::string> box(std::string("chai"));
const Box<std::string>& view = box;
std::println("box.value() {}", kind<decltype(box.value())>());
std::println("view.value() {}", kind<decltype(view.value())>());
std::println("std::move(box).value() {}", kind<decltype(std::move(box).value())>());
std::println("std::move(view).value() {}", kind<decltype(std::move(view).value())>());
box.value() += " and samosa"; // a reference into the box
std::println("in the box: {}", view.value());
std::string taken = std::move(box).value(); // moved out of a box that is going away
std::println("taken: {}", taken);
} from session import run
# one_value.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra one_value.cpp -o one_value && ./one_value
g++-16 -std=c++23 -Wall -Wextra one_value.cpp -o one_value-gcc && ./one_value-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra one_value.cpp -o one_value && ./one_value box.value() std::string& view.value() const std::string& std::move(box).value() std::string&& std::move(view).value() const std::string&& in the box: chai and samosa taken: chai and samosa $ g++-16 -std=c++23 -Wall -Wextra one_value.cpp -o one_value-gcc && ./one_value-gcc box.value() std::string& view.value() const std::string& std::move(box).value() std::string&& std::move(view).value() const std::string&& in the box: chai and samosa taken: chai and samosa
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 value_session.py
this Self&& self is a forwarding reference, so Self records what kind of object the call was made on.
std::forward_like<Self> (C++23, paper P2445R1) then returns the member with the same constness and value category.
For a Box<std::string>:
box.value()deducesSelfasBox&and returnsstd::string&.view.value(), whereviewis aconst Box&, deducesconst Box&and returnsconst std::string&.std::move(box).value()deducesBoxand returnsstd::string&&.std::move(view).value()deducesconst Boxand returnsconst std::string&&.
That is why box.value() += " and samosa" changes the string inside the box, and why
std::string taken = std::move(box).value(); moves the string out instead of copying it. Returning self.value_
without std::forward_like would give an lvalue reference in every case, so the rvalue call would copy the string
and the box would never lend its contents to a move. The two cases that a single const member function cannot
express, writing through a non-const object and moving out of an rvalue, are exactly what the duplicated overloads
used to exist for.
Recursive lambdas
A lambda has no name inside its own body, so before C++23 it could not call itself directly. With an explicit
object parameter, the lambda receives itself as self and recurses through it:
// Lambdas that call themselves through their explicit object parameter.
#include <functional>
#include <print>
#include <vector>
int main() {
auto factorial = [](this auto self, long long n) -> long long { return n <= 1 ? 1 : n * self(n - 1); };
std::println("20! = {}", factorial(20));
// Depth-first search: the order in which the nodes of a small graph are first reached from node 0.
std::vector<std::vector<int>> next{{1, 2}, {3}, {3, 4}, {}, {}};
std::vector<bool> seen(next.size(), false);
std::vector<int> order;
auto dfs = [&](this const auto& self, int u) -> void {
seen[u] = true;
order.push_back(u);
for (int v : next[u])
if (!seen[v]) self(v);
};
dfs(0);
std::println("visit order {}", order);
// The older way: a std::function that the lambda captures, so that it can call itself.
std::function<void(int)> old_dfs = [&](int u) {
seen[u] = true;
for (int v : next[u])
if (!seen[v]) old_dfs(v);
};
std::println("sizeof(dfs) = {}, sizeof(std::function<void(int)>) = {}", sizeof(dfs), sizeof(old_dfs));
} from session import run
# recursive_lambda.cpp built and run with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra recursive_lambda.cpp -o recursive_lambda && ./recursive_lambda
g++-16 -std=c++23 -Wall -Wextra recursive_lambda.cpp -o recursive_lambda-gcc && ./recursive_lambda-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra recursive_lambda.cpp -o recursive_lambda && ./recursive_lambda 20! = 2432902008176640000 visit order [0, 1, 3, 2, 4] sizeof(dfs) = 24, sizeof(std::function<void(int)>) = 32 $ g++-16 -std=c++23 -Wall -Wextra recursive_lambda.cpp -o recursive_lambda-gcc && ./recursive_lambda-gcc 20! = 2432902008176640000 visit order [0, 1, 3, 2, 4] sizeof(dfs) = 24, sizeof(std::function<void(int)>) = 32
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lambda_session.py
factorial takes this auto self by value, a copy of a lambda that captures nothing, which costs nothing. dfs
captures three variables by reference, and each call self(v) goes one level deeper into the graph, so it reaches
the nodes in the order 0, 1, 3, 2, 4. It takes this const auto& self, which never copies the lambda; that matters
once a lambda captures something large by value, because a by-value self copies all its captures at every level
of recursion.
The older workaround stored the lambda in a std::function that the lambda captured by reference. It works, but
std::function erases the lambda’s type, calls it through an indirect pointer that the optimiser can rarely inline,
and may allocate memory for larger lambdas. The last line measures the two objects with both libraries: the lambda
is 24 bytes, its three references, and the std::function 32.
Replacing the curiously recurring template pattern
CRTP lets a base class call functions of the class derived from it: the base is a template, the derived class names
itself as the template argument, and the base casts *this to that type. An explicit object parameter gives the
same result with an ordinary base class, because self is deduced as the type of the object the call is made on:
// The same mixin twice: with the curiously recurring template pattern, and with an explicit object parameter.
#include <print>
#include <string>
// CRTP: the base is a template that is told the derived type, and casts *this to it.
template <class Derived>
struct DescribableCrtp {
void describe() const {
const auto& self = static_cast<const Derived&>(*this);
std::println("{} with area {}", self.name(), self.area());
}
};
// C++23: an ordinary base class; self is deduced as the type of the object it is called on.
struct Describable {
void describe(this const auto& self) { std::println("{} with area {}", self.name(), self.area()); }
};
struct Square : DescribableCrtp<Square> {
double side = 3;
std::string name() const { return "square"; }
double area() const { return side * side; }
};
struct Rectangle : Describable {
double w = 4, h = 2.5;
std::string name() const { return "rectangle"; }
double area() const { return w * h; }
};
int main() {
Square{}.describe();
Rectangle{}.describe();
} // Through a reference to the base class, self is deduced as the base, which has no name() or area().
#include <print>
#include <string>
struct Describable {
void describe(this const auto& self) { std::println("{} with area {}", self.name(), self.area()); }
};
struct Rectangle : Describable {
double w = 4, h = 2.5;
std::string name() const { return "rectangle"; }
double area() const { return w * h; }
};
void show(const Describable& d) { d.describe(); } // error: Describable has no member name()
int main() { show(Rectangle{}); } from session import run
# static_polymorphism.cpp with both compilers, then the errors each one reports in base_reference.cpp.
run("""
clang++ -std=c++23 -Wall -Wextra static_polymorphism.cpp -o static_polymorphism && ./static_polymorphism
g++-16 -std=c++23 -Wall -Wextra static_polymorphism.cpp -o static_polymorphism-gcc && ./static_polymorphism-gcc
clang++ -std=c++23 -fsyntax-only base_reference.cpp 2>&1 | grep -E "^base_reference.cpp:[0-9:]+ error"
g++-16 -std=c++23 -fsyntax-only base_reference.cpp 2>&1 | grep -E "^base_reference.cpp:[0-9:]+ error"
""") Output
$ clang++ -std=c++23 -Wall -Wextra static_polymorphism.cpp -o static_polymorphism && ./static_polymorphism square with area 9 rectangle with area 10 $ g++-16 -std=c++23 -Wall -Wextra static_polymorphism.cpp -o static_polymorphism-gcc && ./static_polymorphism-gcc square with area 9 rectangle with area 10 $ clang++ -std=c++23 -fsyntax-only base_reference.cpp 2>&1 | grep -E "^base_reference.cpp:[0-9:]+ error" base_reference.cpp:6:81: error: no member named 'name' in 'Describable' $ g++-16 -std=c++23 -fsyntax-only base_reference.cpp 2>&1 | grep -E "^base_reference.cpp:[0-9:]+ error" base_reference.cpp:6:81: error: 'const struct Describable' has no member named 'name' base_reference.cpp:6:94: error: 'const struct Describable' has no member named 'area'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 crtp_session.py
Square{}.describe() goes through the CRTP base, and Rectangle{}.describe() through the plain base class with
this const auto& self; both print the derived class’s name and area. The C++23 version needs no template argument,
no cast and no chance of naming the wrong derived class, a classic CRTP slip that compiles and then misbehaves.
Both techniques work with the type the compiler knows, not with the type of the object at run time.
base_reference.cpp calls describe() through a const Describable&, so self is deduced as Describable, which
has no name() or area(), and both compilers reject it. When code must handle different shapes through one base
class reference, use virtual functions, which the next module covers. Explicit object parameters replace CRTP where
CRTP was only a way to reach the derived class while compiling.
Your turn: one subscript operator
Names in names.h defines its subscript operator four times, for non-const and const objects, each as an
lvalue and as an rvalue. Replace the four definitions with one member function template that has an explicit object
parameter, keeping every result type exactly as it is:
C header · names.h
// Starter code for "Your turn": replace the four subscript overloads with one explicit-object member.
#pragma once
#include <cstddef>
#include <string>
#include <utility>
#include <vector>
class Names {
public:
explicit Names(std::vector<std::string> v) : items_(std::move(v)) {}
// One overload for each kind of object: non-const and const, lvalue and rvalue.
std::string& operator[](std::size_t i) & { return items_[i]; }
const std::string& operator[](std::size_t i) const& { return items_[i]; }
std::string&& operator[](std::size_t i) && { return std::move(items_[i]); }
const std::string&& operator[](std::size_t i) const&& { return std::move(items_[i]); }
std::size_t size() const { return items_.size(); }
private:
std::vector<std::string> items_;
}; C++ · names_tests.cpp
// Sample tests for names.h. Build: clang++ -std=c++23 -Wall -Wextra names_tests.cpp -o names_tests
#include <fstream>
#include <print>
#include <sstream>
#include <string>
#include <type_traits>
#include <utility>
#include "names.h"
int checks = 0;
int failures = 0;
void check(bool ok, const char* what) {
++checks;
if (!ok) {
++failures;
std::println("FAIL {}", what);
}
}
// How many times names.h spells out the subscript operator's name.
int definitions_in_header() {
std::ifstream file("names.h");
std::stringstream text;
text << file.rdbuf();
std::string s = text.str();
int count = 0;
for (std::size_t at = s.find("operator[]"); at != std::string::npos; at = s.find("operator[]", at + 1)) ++count;
return count;
}
int main() {
Names n({"Asha", "Dev", "Meera"});
const Names& c = n;
check(std::is_same_v<decltype(n[0]), std::string&>, "a non-const Names gives std::string&");
check(std::is_same_v<decltype(c[0]), const std::string&>, "a const Names gives const std::string&");
check(std::is_same_v<decltype(std::move(n)[0]), std::string&&>, "an rvalue Names gives std::string&&");
check(std::is_same_v<decltype(std::move(c)[0]), const std::string&&>, "a const rvalue Names gives const std::string&&");
n[0] = "Aarav";
check(c[0] == "Aarav", "writing through n[0] changes the element");
std::string moved = std::move(n)[1];
check(moved == "Dev", "std::move(n)[1] hands over the element");
check(n.size() == 3, "moving one element out keeps the size");
int defs = definitions_in_header();
check(defs == 1, "names.h defines the subscript operator once");
if (defs != 1) std::println(" names.h spells operator[] {} times", defs);
std::println("{} of {} checks passed", checks - failures, checks);
return failures == 0 ? 0 : 1;
} The tests check the four result types with std::is_same_v, check that writing and moving through the operator
still work, and read names.h itself to count how often it spells out the operator’s name. The starter code passes
every check but that last one:
from session import run
# The sample tests run against the starter code, before it is rewritten.
run("""
clang++ -std=c++23 -Wall -Wextra names_tests.cpp -o names_tests && ./names_tests
g++-16 -std=c++23 -Wall -Wextra names_tests.cpp -o names_tests-gcc && ./names_tests-gcc
""") Output
$ clang++ -std=c++23 -Wall -Wextra names_tests.cpp -o names_tests && ./names_tests FAIL names.h defines the subscript operator once names.h spells operator[] 4 times 7 of 8 checks passed [exit status 1] $ g++-16 -std=c++23 -Wall -Wextra names_tests.cpp -o names_tests-gcc && ./names_tests-gcc FAIL names.h defines the subscript operator once names.h spells operator[] 4 times 7 of 8 checks passed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 names_session.py
This is a refactoring: the behaviour checks pass before you start, and they must still pass when you finish, now
with 8 of 8 checks passed. Mind the hint in the previous section: return self.items_[i]; on its own compiles,
yet it returns an lvalue reference even for an rvalue Names, which fails the two rvalue type checks. Keep the
comments of names.h free of the operator’s name, since the test counts every occurrence.
Interview questions
Fresher · What does this auto&& self deduce for a non-const lvalue, a const lvalue and an rvalue object?
this auto&& self is a forwarding reference, so it deduces from how the function is called. On a non-const lvalue
obj, self is T&; on a const lvalue it is const T&; on an rvalue such as std::move(obj) or a temporary it
is T&&, where T is the object’s type. The template parameter behind auto, written out as Self, keeps that
information, so std::forward<Self>(self) passes the object on as it came, and std::forward_like<Self>(self.member)
gives a member the same constness and value category. That is what lets one function replace the &, const&,
&& and const&& overloads. A good answer also notes that T can be a class derived from the one that declares the
function, because the type is deduced from the object the function is called on.
Key takeaways
- A C++23 member function can declare its object as the first parameter,
this Type self, and must then useself.member; there is no implicitthisinside it. - The parameter’s type replaces
constand reference qualifiers:this const T&reads,this T&writes, andthis Tworks on a copy. template <class Self> auto&& f(this Self&& self)withstd::forward_like<Self>replaces four overloads.- A lambda with
this auto self, orthis const auto& selfwhen it captures much, can call itself withoutstd::function. - Deduction uses the static type at each call, so explicit object parameters replace CRTP but not virtual functions.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- Working Draft, Programming Languages: C++ (N4950) (ISO/IEC JTC1/SC22/WG21)
- P0847R7: Deducing this (ISO/IEC JTC1/SC22/WG21)
- P2445R1: std::forward_like (ISO/IEC JTC1/SC22/WG21)
- C++ Standards Support in GCC (GNU Project)
- C++ Support in Clang (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress