deducing this

C++
Author

Quasar

Published

June 20, 2025

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;
}

Compiler Explorer

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;
}

Compiler Explorer

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;
}

Compiler Explorer

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;
}

Compiler Explorer

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;
}

Compiler Explorer

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;
}

Compiler Explorer

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.