C++ this Pointer: Chaining, const Methods, Lambdas, and CRTP
이 글의 핵심
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
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:
// Parameter 'width' shadows member 'width_' — no conflict here
Rectangle(int width, int height)
: width_(width), height_(height) {} // initializer list preferred
// If you wrote the assignment manually:
void setSize(int width, int height) {
this->width_ = width; // explicit: left is member, right is parameter
this->height_ = height;
}
};
Initializer lists (: width_(width)) are the preferred way to initialize members in constructors — they avoid this-> entirely and are more efficient.
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 an lvalue Widget — called on lvalue objects
}
void refQualified() && {
// this is an rvalue Widget — called on temporaries
}
};
Widget w;
w.modify(); // OK
w.read(); // OK
const Widget cw;
// cw.modify(); // compile error — modify() is not const
cw.read(); // OK
Method Chaining with return *this
Returning *this by reference allows calling multiple methods on the same line:
#include <string>
#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.
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.
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.
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.
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:
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 — the Derived object is always fully constructed by the time serialize() is called.
Key Takeaways
thisis an implicit pointer to the current object in non-static member functions — always available, usually not needed explicitly- Disambiguation: use
this->memberwhen a parameter has the same name as a member return *thisenables method chaining (builder pattern, fluent APIs)- Self-assignment check:
if (this == &other) return *this;in copy assignment operators - In
constmethods,thisisconst T*— you cannot modify members - Static member functions have no
this— they belong to the class, not an instance [this]lambda capture: holds a raw pointer — dangling if the lambda outlives the object[*this](C++17): copies the object — safe for async but expensive for large objects
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. The implicit this pointer in non-static member functions: disambiguation, method chaining, self-assignment checks, lambd… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ Classes and Objects: Constructors, Access Control, and](/en/blog/cpp-class-object-beginner/
- [C++ Functions: Parameters, Return Values, Overloading, and](/en/blog/cpp-function-basics/
- [C++ Copy Initialization: The = Form, explicit, and Copy](/en/blog/cpp-copy-initialization/
이 글에서 다루는 키워드 (관련 검색어)
C++, this, pointer, member, class 등으로 검색하시면 이 글이 도움이 됩니다.