C++ / a working model

24 / 163   ·   C++11   ·   8 min

this, Object Qualification, and Chained Interfaces

Keep this sentence

this is the pointer expression through which an implicit-object member function operates on an object; the type it points to changes with the member function's const qualification. Returning *this can build a chained interface, but it does not extend the object's lifetime. A static member function has no this, and capturing this does not mean owning the object.

In this lesson
  1. A pointer expression, not a hidden field
  2. const and ref-qualifiers each govern one thing
  3. Lifetime matters more than spelling
  4. Example
  5. Exercise

A pointer expression, not a hidden field

In an ordinary non-const implicit-object member function, the type of this is X*. In a const member function it is const X*. this is a prvalue expression of pointer type. You cannot assign to this itself, and you must not misstate that property as “the type is always X* const”. Member access such as field can usually be understood as access through the current object.

this is not a pointer field that every object must store. How the implicit object parameter is passed is a matter of calling convention. A static member function has no current object, so it cannot use this. Explicit-object member functions in C++23 also operate on an object through a declared object parameter rather than through implicit this.

const and ref-qualifiers each govern one thing

A const member function restricts modification of ordinary members through that object path. It does not guarantee a globally side-effect-free program, and it does not automatically make an external object pointed to by a member pointer const. The usual way to return the current object is return *this;. When the return type is X&, later operations continue to act on the same object.

The example adds an & ref-qualifier to the mutating operation so that it may be called only on an lvalue. Chained operations then cannot quietly return an lvalue reference from a temporary that is easy to dangle. If rvalues must be supported, design a separate && overload with clear value semantics rather than stuffing every caller into one reference-returning interface.

Lifetime matters more than spelling

Storing this, or capturing this in a lambda, stores an access path; it does not obtain ownership. If a callback runs after the object has been destroyed, accessing members is no longer valid. An asynchronous interface must keep the object alive until the callback finishes, or use a suitable owning object, a weak reference, and an expiration check. Simply adding const does not fix a dangling pointer.

The example verifies that set returns the original object itself, and that a read-only query is invoked through a const reference. The whole chain still depends on the original object remaining alive. Even if a member function body reads no fields, you cannot call an ordinary member function through a null pointer and pretend it is static. Operations that need no object should be declared static.

Pitfalls

  • A reference returned from *this does not extend the lifetime of a temporary. Confirm the original object's lifetime before storing a chained result as a long-lived reference.
  • const qualifies access rights through the current object. It is not a thread-safety guarantee, and it is not a guarantee of deep immutability.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <type_traits>

class Counter {
    int value_ = 0;
public:
    Counter& set(int value) & {
        static_assert(std::is_same<decltype(this), Counter*>::value, "mutable this");
        value_ = value;
        return *this;
    }
    int value() const {
        static_assert(std::is_same<decltype(this), const Counter*>::value, "const this");
        return value_;
    }
    bool same_object(const Counter& other) const { return this == &other; }
};

int main() {
    Counter counter;
    Counter& result = counter.set(2).set(5);
    const Counter& view = counter;
    assert(&result == &counter);
    assert(view.value() == 5);
    assert(view.same_object(result));
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-this.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

If you change set's return type to Counter, why can counter.set(2).set(5) no longer work as before?

Show a reference answer

The first call returns a new Counter value, not a reference to the original object. The receiver of the second call is a temporary, which does not satisfy set's & ref-qualifier, so compilation fails. Even if you drop the ref-qualifier, the second call would modify only a copy, and the original counter would still hold two. A chained interface must consider return type, value category, and object identity together.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog