Introduction
Member functions can be overloaded by cv
-qualifiers and reference qualifiers &
(ref) and &&
(ref-ref).
/*
Member functions can be overloaded by cv-qualifiers and
reference qualifiers.
*/
#include <iostream>
// Implicit
struct X{
void f() &{ std::cout << "\n" << "X::f() &"; }
void f() const&{ std::cout << "\n" << "X::f() const&"; }
void f() && { std::cout << "\n" << "X::f() &&"; }
void f() const&& { std::cout << "\n" << "X::f() const&&"; }
};
//Explicit
struct Y{
void f(this Y&){ std::cout << "\n" << "Y::f() &"; }
void f(this const Y&){ std::cout << "\n" << "Y::f() const&"; }
void f(this Y&&){ std::cout << "\n" << "Y::f() &&"; }
void f(this const Y&&){ std::cout << "\n" << "Y::f() const &&"; }
};
int main(){
X x; Y y;
const X c_x; const Y c_y;
x.f();
c_x.f();
X().f();
const_cast<const X&&>(X()).f();
y.f();
c_y.f();
Y().f();
const_cast<const Y&&>(Y()).f();
return 0;
}
deducing this
feature
If const
and non-const
overloads of a method and (ref)&
and (ref-ref)&&
overloads share the same implementation, then we can de-duplicate these overloads and allow the compiler to automatically deduce the object type, on which the member function was invoked using this feature.
The real value of deducing
this comes from using the type Self
in some way in the body e.g. using std::forward_like<T,U>
to propagate an owning-object’s value category to its member data.
Consider the following example. We are writing a homegrown version of vector<T>
container and want to implement the begin()
method.
#include <iostream>
#include <memory>
#include <type_traits>
#include <concepts>
template<typename T>
struct vector{
T* m_data;
std::size_t m_size;
std::size_t m_capacity;
vector()
: m_data{new T[8]}
, m_size{0}
, m_capacity{8}
{}
template<typename U>
struct Iterator{
using difference_type = std::ptrdiff_t;
using value_type = U;
Iterator() = default;
explicit Iterator(U* ptr)
: m_ptr{ptr}
{}
U* m_ptr;
};
using iterator = Iterator<T>;
using const_iterator = Iterator<const T>;
auto begin(this auto&& self){
return Iterator(self.m_data);
// ^----------
// T* const, if self is const
// T*, otherwise
}
auto end(this auto&& self){
return Iterator(self.m_data + self.m_size);
}
~vector(){
delete[] m_data;
}
};
int main(){
vector<double> v;
const vector<double> cv;
static_assert(
std::is_same_v<
decltype(v.begin()),
vector<double>::Iterator<double>
>
);
static_assert(
std::is_same_v<
decltype(cv.begin()),
vector<double>::Iterator<double>
>
);
return 0;
}
Instead of writing traditional const
and non-const
variants of begin()
, we are using the deducing this
feature to de-duplicate overloads.
While at the outset, this code might look fine, be warned that std::is_same_v<decltype(cv.begin()),vector<double>::Iterator<const double>>
returns false_type
. T* m_data
of a const
object becomes T* const m_data
, that is const
ends up on the top-level of that type. const
qualifiers on the top-level Iterator
class are then discarded when template argument deduction is performed on the implicit function-template powering the constructor call Iterator(self.m_data)
. The client may write hostile code and modify the contents of the const vector
through the iterator object. What we want is a pointer-to-const T
instead of a const
-pointer-to-T
.
We can branch on the const
-ness of self
and return the correct iterator type.
#include <iostream>
#include <memory>
#include <type_traits>
#include <concepts>
template<typename T>
struct vector{
T* m_data;
std::size_t m_size;
std::size_t m_capacity;
template<typename U>
struct Iterator{
using difference_type = std::ptrdiff_t;
using value_type = U;
using reference = U&;
using const_reference = const U&;
using pointer = U*;
U* m_ptr;
Iterator() = default;
explicit Iterator(U* ptr)
: m_ptr{ptr}
{}
Iterator& operator++(){
++m_ptr;
return *this;
}
Iterator operator++(int){
auto temp {*this};
++m_ptr;
return temp;
}
Iterator operator+(int n){
return Iterator(m_ptr + n);
}
U& operator*(){
return *m_ptr;
}
auto operator<=>(const Iterator& other) const{
return m_ptr<=>other.m_ptr;
}
bool operator==(const Iterator& other) const{
return m_ptr==other.m_ptr;
}
};
using iterator = Iterator<T>;
using const_iterator = Iterator<const T>;
auto begin(this auto&& self){
if constexpr(std::is_const_v<std::remove_reference_t<decltype(self)>>)
return const_iterator(self.m_data);
else
return iterator(self.m_data);
}
auto end(this auto&& self){
if constexpr(std::is_const_v<std::remove_reference_t<decltype(self)>>)
return const_iterator(self.m_data + self.m_size);
else
return iterator(self.m_data);
}
vector(std::size_t n, const T& x)
: m_data{ static_cast<T*>(::operator new(sizeof(T) * n)) }
, m_size{0}
, m_capacity{n}
{
auto p{begin()};
try{
for(;p != (begin()+n);++p)
new(static_cast<void*>(p.m_ptr)) T(x);
}
catch(...){
for(auto q{begin()}; q!=p; ++q)
q.m_ptr->~T();
::operator delete (m_data);
throw;
}
m_size = n;
}
~vector(){
std::destroy(begin(), end());
::operator delete (m_data);
}
};
template<typename IterType>
void foo(IterType iter){
std::cout << "\n" << "foo(IterType iter)" << ", *iter = " << *iter;
}
int main(){
vector v(5,2.0);
const vector cv(5,2.0);
static_assert(
std::is_same_v<
decltype(v.begin()),
vector<double>::Iterator<double>
>
);
static_assert(
std::is_same_v<
decltype(cv.begin()),
vector<double>::Iterator<const double>
>
);
foo(vector(10,2.0).begin());
return 0;
}
A small digression - auto
deduction rules
In the below code snippet, can you tell why the static assertion passes?
#include <iostream>
#include <concepts>
#include <type_traits>
template<typename T>
struct Wrapper{
T* m_data;
Wrapper(const T& val)
: m_data{new T(val)}
{}
auto get(this Wrapper& self){
return self.m_data;
}
auto get(this Wrapper const& self){
return self.m_data;
}
auto get(this Wrapper&& self){
return self.m_data;
}
auto get(this Wrapper const&& self){
return self.m_data;
}
};
int main(){
Wrapper<int> wrapper{5};
const Wrapper<int> const_wrapper{42};
static_assert(std::is_same_v<
decltype(const_wrapper.get()),
int*
>);
// const_wrapper.m_data = new int(10); m_data is immutable
*const_wrapper.m_data = 10;
return 0;
}
The static assertion passes, because get()
returns auto
. It is, as if, we are returning by value. auto
always deduces a non-const
, non-reference object.
Propagating the value category of the owning object
Consider a highly simplified version of optional<T>
which is a wrapper type for representing nullable T
objects which may/may not contain a value.
template<typename T>
struct optional{
T m_storage;
bool m_is_initialized;
optional(T const& v)
: m_storage{}
, m_is_initialized(true)
{
::operator new (static_cast<void*>&m_storage) T(v));
}
optional()
: m_storage{}
, m_is_initialized{false}
{}
/* ... */
};
From a design perspective, we would like that getters such as optional<T>::get()
should propagate the value-category of the owning object to it’s member data.
#include <utility>
#include <iostream>
#include <type_traits>
template<typename T>
struct optional{
T m_storage;
bool m_is_initialized;
optional(T const& v)
: m_storage{}
, m_is_initialized(true)
{
::new (static_cast<void*>(&m_storage)) T(v); // placement new
}
optional()
: m_storage{}
, m_is_initialized{false}
{}
template<typename Self>
decltype(auto) value(this Self&& self){
return std::forward<decltype(self)>(self).m_storage;
}
};
int main(){
optional<double> opt_d(42.0);
opt_d.value();
const optional<double> copt_d(5.0);
copt_d.value();
optional<double>(17.0).value();
static_cast<optional<double>&&>(optional<double>(28.0)).value();
return 0;
}
One function template does it. This is equivalent to writing:
#include <utility>
#include <iostream>
template<typename T>
struct optional{
T m_storage;
bool m_is_initialized;
optional(T const& v)
: m_storage(v)
, m_is_initialized(true)
{}
optional()
: m_storage{}
, m_is_initialized{false}
{}
/*
template<typename Self>
decltype(auto) value(this Self&& self){
return std::forward<decltype(self)>(self).m_storage;
}
*/
decltype(auto) value(this optional& self){
std::cout << "\n" << "value(this optional&)";
return self.m_storage;
}
decltype(auto) value(this optional const& self){
std::cout << "\n" << "value(this optional const&)";
return (self.m_storage);
}
decltype(auto) value(this optional&& self){
std::cout << "\n" << "value(this optional &&)";
return (std::move(self).m_storage);
}
decltype(auto) value(this optional const&& self){
std::cout << "\n" << "value(this optional const &&)";
return (std::move(self).m_storage);
}
};
int main(){
optional<double> opt_d(42.0);
opt_d.value();
const optional<double> copt_d(5.0);
copt_d.value();
optional<double>(17.0).value();
static_cast<optional<double> const&&>(optional<double>(28.0)).value();
return 0;
}
Getter return types
If we need to propagate both const
-ness and the value category of the owning object o
to its T m_data
data-member, we could use std::forward_like<T,U>
defined in the <utility>
header.