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 const
–at()
, 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.