C++ this Pointer: Chaining, const Methods, Lambdas, and CRTP
Key takeaways
The implicit this pointer in non-static member functions: disambiguation, method chaining, self-assignment checks, lambda captures, and const/ref qualifiers. Complete guide with practical examples.
What is this?
In any non-static member function, this is an implicit pointer to the object the method was called on. The compiler passes it automatically — you don’t declare it, but you can use it:
class Counter {
int count_;
public:
Counter() : count_(0) {}
void increment() {
count_++; // implicit: this->count_++
}
int value() const {
return count_; // implicit: return this->count_
}
};
Counter c;
c.increment(); // compiler calls: increment(&c) — passes &c as this
That “hidden first parameter” is close to what actually happens in the generated code: on x86-64 Linux the object’s address arrives in the first argument register (rdi), exactly like an explicit pointer parameter would. A member function is compiled once and shared by all objects; this is how the one copy of the code knows which object’s count_ to touch. It also explains why this is a prvalue — you cannot assign to it (this = nullptr; is a compile error) — and why you cannot take its address.
The FAQ below mentions that calling a member function through a null pointer is undefined behavior even if the function never touches a member. That is not only theoretical: because the compiler may assume this is never null, optimizers delete checks like if (this == nullptr) return; inside member functions. GCC does this since version 6, and code that relied on such checks (some older codebases did) started crashing after compiler upgrades.
Most of the time you don’t need this explicitly — the compiler finds members automatically. You need this when:
- A parameter has the same name as a member (disambiguation)
- You want to return the current object (
return *this) - You need to check for self-assignment (
this == &other) - You’re passing the object to a callback or storing a pointer to it
Disambiguation: Parameter vs Member
When a constructor or setter parameter shares a name with a member, this-> distinguishes them:
class Rectangle {
int width;
int height;
public:
// In an initializer list, the name outside the parentheses is always
// the member, so the same name works without this->
Rectangle(int width, int height)
: width(width), height(height) {}
// In a function body, the parameter hides the member:
void setSize(int width, int height) {
this->width = width; // left: member, right: parameter
this->height = height;
// width = width; // assigns the parameter to itself — member unchanged
}
};
Initializer lists (: width(width)) are the preferred way to initialize members in constructors — they avoid this-> entirely and are more efficient.
The commented-out line is the actual bug this section is about: width = width; compiles, assigns the parameter to itself, and leaves the member untouched, so the rectangle keeps its old size. Clang warns about it (-Wself-assign), GCC only with -Wshadow on the declaration. Many codebases avoid the situation entirely with a naming convention — a trailing underscore (width_), an m_ prefix — so that members and parameters can never collide and this-> is never needed for disambiguation.
The Type of this
this changes type depending on the qualifiers of the member function:
class Widget {
int data_ = 0;
public:
void modify() {
// this is: Widget* (non-const, can modify members)
data_ = 42;
}
void read() const {
// this is: const Widget* (cannot modify members)
// data_ = 42; // compile error
}
void refQualified() & {
// this is still Widget*; this overload is chosen when *this is an lvalue
}
void refQualified() && {
// chosen when called on a temporary (an rvalue), e.g. Widget{}.refQualified()
}
};
Widget w;
w.modify(); // OK
w.read(); // OK
const Widget cw;
// cw.modify(); // compile error — modify() is not const
cw.read(); // OK
Strictly, inside a non-const member function this has type Widget*, and inside a const one it is const Widget* — the pointer itself is never modifiable, and the const applies to the object it points to. This is the whole mechanism behind const-correctness: a const member function can only call other const member functions, because passing its const Widget* to a function expecting Widget* would discard the qualifier. The error message says exactly that: “passing ‘const Widget’ as ‘this’ argument discards qualifiers”.
The ref-qualified overloads are useful when a method can be cheaper on a temporary. A getter like std::string name() && can return std::move(name_); because the object is about to die anyway, while const std::string& name() const & returns a reference for lvalues. Once one overload of a function has a ref-qualifier, all overloads with the same name must have one.
Method Chaining with return *this
Returning *this by reference allows calling multiple methods on the same line:
#include <string>
#include <vector>
#include <iostream>
class QueryBuilder {
std::string query_;
std::vector<std::string> conditions_;
int limit_ = -1;
public:
QueryBuilder& from(const std::string& table) {
query_ = "SELECT * FROM " + table;
return *this;
}
QueryBuilder& where(const std::string& condition) {
conditions_.push_back(condition);
return *this;
}
QueryBuilder& limit(int n) {
limit_ = n;
return *this;
}
std::string build() const {
std::string sql = query_;
for (size_t i = 0; i < conditions_.size(); i++) {
sql += (i == 0 ? " WHERE " : " AND ") + conditions_[i];
}
if (limit_ > 0) sql += " LIMIT " + std::to_string(limit_);
return sql;
}
};
int main() {
auto sql = QueryBuilder{}
.from("users")
.where("age > 18")
.where("active = true")
.limit(10)
.build();
std::cout << sql << '\n';
// SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10
}
The fluent builder pattern is one of the most common uses of return *this.
The return type must be a reference (QueryBuilder&). If where returned QueryBuilder by value, each call in the chain would operate on a fresh copy, and the chain would still compile and appear to work — only the final copy would contain all conditions, and any builder you kept in a variable would silently miss them. The chain in main is also safe only because it ends in .build() within the same expression: QueryBuilder{} is a temporary that lives until the end of the full expression. auto& qb = QueryBuilder{}.from("users"); compiles and leaves qb referring to a destroyed object.
As a side note, building SQL by concatenating condition strings is fine for illustrating chaining, but in real code conditions should carry bound parameters rather than values pasted into the string, or user input becomes an SQL injection vector.
Self-Assignment Check in operator=
When implementing a copy assignment operator, check for self-assignment first using this:
class Buffer {
char* data_;
size_t size_;
public:
Buffer(size_t n) : data_(new char[n]), size_(n) {}
~Buffer() { delete[] data_; }
Buffer& operator=(const Buffer& other) {
if (this == &other) return *this; // guard against self-assignment
// Would be unsafe without the guard:
// delete[] data_; // frees other.data_ if this == &other
// data_ = new char[other.size_]; // new allocation
// std::copy(other.data_, ...); // reads freed memory!
delete[] data_;
size_ = other.size_;
data_ = new char[size_];
std::copy(other.data_, other.data_ + size_, data_);
return *this;
}
};
Self-assignment happens more often than you’d expect — a = a is the obvious case, but v[i] = v[j] when i == j is a subtle one. Aliasing through pointers and references (*p = *q, or a function assign(Buffer& a, const Buffer& b) called with the same object twice) produces the same situation without anything looking suspicious at the call site.
The check compares addresses, which is why it uses this rather than comparing contents: two different buffers with equal contents are not a problem, only the same object is. As the FAQ at the end points out, the check prevents the self-assignment crash but not the exception-safety problem, and copy-and-swap handles both without any explicit check.
Passing this to Callbacks
When registering an object as a callback, you pass this to store a pointer to the current instance:
#include <functional>
#include <iostream>
class Button {
std::function<void()> onClick_;
public:
void setCallback(std::function<void()> cb) {
onClick_ = cb;
}
void click() {
if (onClick_) onClick_();
}
};
class App {
int clickCount_ = 0;
Button button_;
public:
App() {
// Pass this to capture App in the lambda
button_.setCallback([this]() {
clickCount_++;
std::cout << "Clicked " << clickCount_ << " times\n";
});
}
void run() {
button_.click();
button_.click();
button_.click();
}
};
int main() {
App app;
app.run();
// Clicked 1 times
// Clicked 2 times
// Clicked 3 times
}
Lifetime warning: the lambda captures a raw pointer (this). If the App object is destroyed before the Button callback is triggered (e.g., in async code), the lambda will call a dangling pointer.
There is a second, less obvious way this example breaks: copying or moving App. The default copy constructor copies button_, including its std::function, and the copied lambda still holds the original object’s this. So after App b = a;, clicking b.button_ increments a.clickCount_; and if a is a temporary returned from a factory function (without guaranteed copy elision), the moved-into App has a button whose callback points at the dead temporary. This is one of the most common real-world this-capture bugs, and it survives testing because the obvious usage (App app; on the stack, never copied) works. Either delete the copy and move operations for classes that register this in callbacks, or re-register the callback in the copy/move constructors.
Lambdas and this
class EventHandler {
std::string name_;
std::function<void()> handler_;
public:
EventHandler(const std::string& name) : name_(name) {
// [this] — captures pointer; safe only if lambda won't outlive the object
handler_ = [this]() {
std::cout << "Event from: " << name_ << '\n';
};
// [*this] (C++17) — copies the object into the lambda
// Safe even if EventHandler is destroyed, but copies all members
auto safeHandler = [*this]() {
std::cout << "Event from: " << name_ << '\n';
};
}
void trigger() { handler_(); }
};
Use [this] when you know the lambda’s lifetime is bounded by the object’s lifetime.
Use [*this] when the lambda might outlive the object (async callbacks, stored futures) — but beware the copy cost for large objects.
[*this] gives the lambda a snapshot. Changes made to the original object after the lambda was created are invisible to it, and changes the lambda makes (with mutable) do not affect the original. That is the right semantics for “log this state later”, but wrong for “update this object when the network reply arrives”. For the second case, the usual answer is shared ownership: make the class inherit from std::enable_shared_from_this, create instances with std::make_shared, and capture weak_from_this() in the lambda, locking it when the callback runs. The callback then either finds a live object or knows it has gone away.
Static Member Functions Have No this
Static member functions belong to the class, not any instance. They have no this:
class Config {
static Config* instance_;
int port_ = 8080;
Config() = default;
public:
// Static factory — no this, no instance needed
static Config& get() {
if (!instance_) instance_ = new Config();
return *instance_;
}
// Non-static — has this, accesses instance data
int port() const { return port_; }
};
Config* Config::instance_ = nullptr;
int main() {
Config::get().port(); // static function returns instance; then non-static call
}
If you call a non-static method through a null pointer, it’s undefined behavior — even if the method doesn’t actually access members, the behavior is still technically UB.
this in Derived Classes and CRTP
In derived classes, this has the type of the derived class in methods defined there. CRTP (Curiously Recurring Template Pattern) uses static_cast<Derived*>(this) to call derived class methods from a base class template:
#include <iostream>
#include <string>
template<typename Derived>
class Serializable {
public:
std::string serialize() const {
// Cast this to Derived* to call derived class method
const Derived& d = static_cast<const Derived&>(*this);
return d.toJson(); // calls Derived::toJson()
}
};
class User : public Serializable<User> {
std::string name_;
int age_;
public:
User(std::string name, int age) : name_(std::move(name)), age_(age) {}
std::string toJson() const {
return "{\"name\":\"" + name_ + "\",\"age\":" + std::to_string(age_) + "}";
}
};
int main() {
User u("Alice", 30);
std::cout << u.serialize() << '\n';
// {"name":"Alice","age":30}
}
The static_cast is safe here because Serializable<User> is always used as a base of User and serialize() is only called on a fully constructed User. Two ways to break that assumption: calling serialize() from Serializable’s own constructor (the User part does not exist yet, so the cast refers to an unconstructed object), and a typo such as class Admin : public Serializable<User>, which compiles and casts an Admin to User — undefined behavior. A common guard is to make Serializable’s constructor private and add friend Derived;, so only the intended derived class can inherit from it.
CRTP gives compile-time polymorphism: serialize() calls toJson() directly, with no virtual table and full inlining, at the cost of having no common base type — a Serializable<User> and a Serializable<Post> cannot be stored in the same container. C++23’s explicit object parameter (“deducing this”) makes the pattern simpler: a non-template base can declare std::string serialize(this const auto& self) { return self.toJson(); }, and self has the derived type automatically, with no cast at all.
Frequently Asked Questions (FAQ)
Q. Is the self-assignment check enough to make operator= safe?
A. No. In the Buffer example, delete[] data_ runs before new char[size_], so if the allocation throws, data_ is left dangling and the destructor frees it a second time. Allocate and copy into a new buffer first, then delete the old one, or use copy-and-swap, which handles self-assignment correctly without an explicit check (at the cost of an extra copy when it does happen).
Related Articles
- C++ Classes and Objects: Constructors, Access Control, and
- C++ Functions: Parameters, Return Values, Overloading, and
- C++ Copy Initialization: The = Form, explicit, and Copy