Applications of C++23’s deducing ‘this’

All C++ programmers should understand the concept of “const-correctness” which is an additional form of type safety provided by the language. (C99 even incorporated this keyword and feature after C++ introduced it.) A common example of this feature is where classes provide accessors (getters/setters) to member data, with const member functions for the case where member data is logically immutable. A non-const-correct Matrix class allowing element access might look as follows:

template<typename T, size_t X, size_t Y>
struct Matrix {
    std::array<T, X * Y> data{};
    
    T& at(size_t x, size_t y) {
        return data.at(y * X + x);
    }
};

When turning this struct definition into something more production ready, we might be tempted to write a const version of at() with the same body and slightly modified signature const T& at(size_t x, size_t y) const {...}. But this implies “cut-and-pasting code” which in larger projects becomes a maintenance issue. The solution suggested by Scott Meyers is to provide only the const version, and have the non-const version call it:

template<typename T, size_t X, size_t Y>
struct Matrix {
    std::array<T, X * Y> data{};
    
    const T& at(size_t x, size_t y) const { // const element access
        return data.at(y * X + x);
    }
    
    T& at(size_t x, size_t y) { // non-const element access
        return const_cast<T&>(static_cast<const Matrix<T, X, Y>&>(*this).at(x, y));
    }
};

This requires two casts, adding const-ness to *this before calling the constat(), and then casting away const so that mutability is permitted. The casting syntax is a little ugly, however, and C++23 provides a better solution, read on.

The key to understanding the Modern C++ approach to this problem is recognizing what we need to do, that is change the type of *this. So, it follows that being able to deduce *this gives us an advantage. Knowing about forwarding references and template member functions is useful, too. Here is the C++23 equivalent to the second code example above (note that this code was compiled with Visual Studio 2022 C++, support in other compilers may not be available yet):

#include <iostream>
#include <array>

template<typename T, size_t X, size_t Y>
struct Matrix {
    std::array<T, X * Y> data{};

    template<typename Self>
    auto& at(this Self&& self, size_t x, size_t y) {
        return std::forward<Self>(self).data.at(y * X + x);
    }
};

int main() {
    Matrix<int, 2, 3> mat{};
    mat.at(1, 1) = -9;
    std::cout << mat.at(1, 1) << '\n';
}

Notice that member function at() now takes three parameters, not two as before. The new parameter is this Self&& self which declares the *this object as self which we use on the next line. The boilerplate std::forward<Self>(self) is used instead of a static_cast and this maintains the const-ness of the object (assigning to a const Matrix remains illegal). The return type is deduced correctly by auto& (I don’t believe decltype(auto) is necessary here, if you think or know differently please leave a comment). While the function is written only once, the fact it is a template member function means it can be multiply instantiated by the compiler for const/non-const calls.

You may be interested to learn that deducing this has applicability in Modern C++ lambdas, too. It has previously not been possible for a lambda to be a recursive function (able to call itself); this is now possible with the syntax made available in C++23. An extra parameter when defining the lambda which creates an alias for this inside the lambda’s body is all that is needed. I’m sure many C++ programmers will find interesting use cases for this feature, to whet your appetite here is the factorial function n! written as a generic lambda (code tested on VS2022 C++):

auto factorial = [](this const auto self, const auto n){
    if (n < 2) {
        return 1;
    }
    else {
        return n * self(n - 1); // same as: return n * factorial(n - 1);
    }
};

static_assert(factorial(5) == 120, "factorial() incorrect");

In summary, deducing this as a named variable can be achieved in lambdas and non-static member functions with the syntax: this <Type> <Alias> as an additional first parameter. The alias variable can be forwarded or invoked as required as any other variable of the object’s type within lambdas and member functions.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s