Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 5 of 12

Every non-static member function receives a hidden pointer to the object it was called on, named this.

#include <iostream>

class Counter {
    int count = 0;

public:
    void increment() {
        ++count;          // shorthand
        // ++this->count; // what it actually means
    }

    int value() const { return this->count; }
};

int main() {
    Counter c;
    c.increment();
    c.increment();
    std::cout << c.value() << "\n";
    return 0;
}
2

Writing this-> everywhere is legal and noisy. Use the bare member name unless you need this for one of the reasons below.

this is a pointer, not a reference, for historical reasons — references did not exist when it was designed. In a const member function its type is const T*, which is exactly why such a function cannot modify members.

When you actually need it

1. A parameter shadows a member.

#include <iostream>

class Point {
    int x;
    int y;

public:
    // Parameters have the same names as the members.
    Point(int x, int y) : x(x), y(y) {}

    void move_to(int x, int y) {
        this->x = x;      // without `this->`, this would assign the parameter to itself
        this->y = y;
    }

    void print() const { std::cout << x << "," << y << "\n"; }
};

int main() {
    Point p(1, 2);
    p.print();
    p.move_to(10, 20);
    p.print();
    return 0;
}
1,2
10,20

Note the constructor's : x(x), y(y) works without this-> — in a member initializer list, the name outside the parentheses is always the member and the name inside is the parameter. Inside a function body there is no such rule, so x = x; assigns the parameter to itself and the member is never touched.

GCC does not warn about this, even with -Wall -Wextra. Clang has -Wself-assign, which GCC does not accept at all. So this is a silent bug on the most common toolchain: the code compiles, runs, and quietly does nothing.

The reliable fix is to remove the shadowing rather than rely on a diagnostic. Many codebases name members x_ or m_x for exactly this reason.

2. Returning the object for chaining.

#include <iostream>
#include <string>

class Builder {
    std::string result;

public:
    Builder& add(const std::string& part) {
        result += part;
        return *this;          // return the object itself, by reference
    }

    Builder& add_space() {
        result += ' ';
        return *this;
    }

    const std::string& str() const { return result; }
};

int main() {
    Builder b;
    b.add("Evil").add_space().add("Genius").add_space().add("Society");
    std::cout << b.str() << "\n";
    return 0;
}
Evil Genius Society

return *this; dereferences the pointer to give the object, returned by reference so no copy is made and the next call operates on the same object. This is how std::cout << a << b chains, and how builder-style APIs work.

Returning Builder by value instead of Builder& would copy the object at every step and each call would modify a different temporary — a bug that still compiles.

3. Passing yourself to something else.

#include <iostream>

class Widget;

void describe(const Widget& w);

class Widget {
    int id;

public:
    Widget(int i) : id(i) {}
    int get_id() const { return id; }

    void report() const {
        describe(*this);      // hand the whole object over
    }
};

void describe(const Widget& w) {
    std::cout << "widget #" << w.get_id() << "\n";
}

int main() {
    Widget w(42);
    w.report();
    return 0;
}
widget #42

4. Checking for self-assignment. Lesson 9.7 needs this:

Buffer& operator=(const Buffer& other) {
    if (this == &other) {
        return *this;      // assigning to itself: do nothing
    }
    // ... otherwise copy
    return *this;
}

Comparing addresses is how you detect b = b;, which without this check would free the buffer and then copy from it.

this in a const member

#include <iostream>

class Example {
    int value = 5;

public:
    void modify() {
        this->value = 10;         // this is Example*
    }

    int read() const {
        // this->value = 1;       // error: this is const Example*
        return this->value;
    }
};

int main() {
    Example e;
    std::cout << e.read() << "\n";
    e.modify();
    std::cout << e.read() << "\n";
    return 0;
}
5
10

The const on the member function is applied to this. That is the whole mechanism — there is nothing else to it.

this is never null

In well-defined code this always points at a real object, so checking if (this == nullptr) is pointless: calling a member function through a null pointer is already undefined behaviour before your check runs, and compilers optimise the check away on exactly that reasoning (lesson 8.7).

Try it

Write a class Vec2 with x and y, and add, scale and normalise_sign members that each return Vec2& so calls can chain. Use this-> where a parameter shadows a member.

Checkpoint

  • Every non-static member function gets a hidden this pointing at the object.
  • Member access is implicitly this->member; write it out only when needed.
  • In a const member, this is const T* — that is how const is enforced.
  • return *this; by reference enables call chaining.
  • this == &other detects self-assignment (lesson 9.7).
  • Use this-> when a parameter shadows a member, or rename members to avoid the clash.

Questions about this lesson

Log in to ask a question.