Now Reading
C++23’s Deducing this: what it’s, why it’s, the way to use it

C++23’s Deducing this: what it’s, why it’s, the way to use it

2023-07-13 05:49:47

Deducing this (P0847) is a C++23 characteristic which provides a brand new manner of specifying non-static member features. Often once we name an object’s member perform, the thing is implicitly handed to the member perform, regardless of not being current within the parameter checklist. P0847 permits us to make this parameter specific, giving it a reputation and const/reference qualifiers. For instance:

struct implicit_style {
    void do_something(); //object is implicit
};

struct explicit_style {
    void do_something(this explicit_style& self); //object is specific
};

The specific object parameter is distinguished by the key phrase this positioned earlier than the sort specifier, and is barely legitimate for the primary parameter of the perform.

The explanations for permitting this will not appear instantly apparent, however a bunch of further options fall out of this virtually by magic. These embody de-quadruplication of code, recursive lambdas, passing this by worth, and a model of the CRTP which doesn’t require the bottom class to be templated on the derived class.

This put up will stroll by way of an summary of the design, then most of the circumstances you should use this characteristic for in your personal code.

For the remainder of this weblog put up I’ll seek advice from the characteristic as “specific object parameters”, because it makes extra sense as a characteristic title than “deducing this“. Specific object parameters are supported in MSVC as of Visible Studio 2022 model 17.2. companion to this put up is Ben Deane’s discuss Deducing this Patterns from CppCon.

Overview

The paper which proposed this characteristic was written by Gašper Ažman, Ben Deane, Barry Revzin, and myself, and was guided by the expertise of many experts in the field. Barry and I started writing a model of this paper after we every carried out std::optional and got here throughout the identical downside. We might be writing the worth perform of elective and, like good library builders, we’d attempt to make it usable and performant in as many use-cases as we may. So we’d need worth to return a const reference if the thing it was known as on was const, we’d need it to return an rvalue if the thing it was known as on was an rvalue, and so on. It ended up wanting like this:

template <typename T>
class elective {
  // model of worth for non-const lvalues
  constexpr T& worth() & {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  // model of worth for const lvalues
  constexpr T const& worth() const& {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  // model of worth for non-const rvalues... are you bored but?
  constexpr T&& worth() && {
    if (has_value()) {
      return std::transfer(this->m_value);
    }
    throw bad_optional_access();
  }

  // you certain are by this level
  constexpr T const&& worth() const&& {
    if (has_value()) {
      return std::transfer(this->m_value);
    }
    throw bad_optional_access();
  }
  // ...
};

(In case you’re not aware of the member_function_name() & syntax, that is known as “ref-qualifiers” and you’ll find extra information on Andrzej Krzemieński’s blog. In case you’re not aware of rvalue references (T&&) you may learn up on transfer semantics on this Stack Overflow question)

Word the near-identical implementations of 4 variations of the identical perform, solely differentiated on whether or not they’re const and whether or not they transfer the saved worth as an alternative of copying it.

Barry and I might then transfer on to another perform and must do the identical factor. And time and again, again and again, duplicating code, making errors, constructing upkeep complications for the long run variations of ourselves. “What if”, we thought, “you possibly can simply write this?”

template <typename T>
struct elective {
  // One model of worth which works for all the things
  template <class Self>
  constexpr auto&& worth(this Self&& self) {
    if (self.has_value()) {
        return std::ahead<Self>(self).m_value;
    }
    throw bad_optional_access();
  }

(In case you’re not aware of std::ahead, you may examine good forwarding on Eli Bendersky’s blog)

This does the identical factor because the above 4 overloads, however in a single perform. As a substitute of writing completely different variations of worth for const elective&, const elective&&, elective&, and elective&&, we write one perform template which deduces the const/unstable/reference (cvref for brief) qualifiers of the thing the it’s known as on. Making this alteration for nearly each perform within the sort would minimize down our code by an enormous quantity.

So we wrote a model of what finally received standardised, quickly found that Gašper and Ben had been engaged on a special paper for the very same characteristic, we joined forces, and right here all of us are a number of years later.

Design

The important thing design precept we adopted was that it ought to do what you count on. To realize this, we touched as few locations in the usual as we presumably may. Notably, we didn’t contact overload decision guidelines or template deduction guidelines, and title decision was solely modified a bit of bit (as a deal with).

As such, say we have now a sort like so:

struct cat {
    template <class Self>
    void lick_paw(this Self&& self);
};

The template parameter Self can be deduced based mostly on the entire identical template deduction guidelines you’re already aware of. There’s no further magic. You don’t have to make use of the names Self and self, however I feel they’re the clearest choices, and this follows what a number of different programming languages do.

cat marshmallow;
marshmallow.lick_paw();                         //Self = cat&

const cat marshmallow_but_stubborn;
marshmallow_but_stubborn.lick_paw();            //Self = const cat&

std::transfer(marshmallow).lick_paw();              //Self = cat
std::transfer(marshmallow_but_stubborn).lick_paw(); //Self = const cat

One title decision change is that inside such a member perform, you aren’t allowed to explicitly or implicitly seek advice from this.

struct cat {
    std::string title;

    void print_name(this const cat& self) {
        std::cout << title;       //invalid
        std::cout << this->title; //additionally invalid
        std::cout << self.title;  //all good
    }
};

Use Circumstances

For the remainder of this put up, we’ll take a look at all of the completely different makes use of of this characteristic (at the very least those found thus far that I do know of!) Many of those examples had been taken straight from the paper.

De-duplication/quadruplication

We’ve already seen how the characteristic could be utilized to a sort resembling elective to keep away from having to jot down 4 overloads of the identical perform.

Word additionally that this lowers the burden on preliminary implementation and upkeep of coping with rvalue member features. Very often builders will write solely const and non-const overloads for member features, since in lots of circumstances we don’t actually wish to write one other two complete features simply to take care of rvalues. With deduced qualifiers on this, we get the rvalue variations totally free: we simply want to jot down std::ahead in the best locations to get the runtime efficiency positive aspects which include avoiding pointless copies:

class cat {
    toy held_toy_;

public:
    //Earlier than specific object parameters
    toy& get_held_toy() { return held_toy_; }
    const toy& get_held_toy() const { return held_toy_; }

    //After
    template <class Self>
    auto&& get_held_toy(this Self&& self) {
        return self.held_toy_;
    }

    //After + forwarding
    template <class Self>
    auto&& get_held_toy(this Self&& self) {
        return std::ahead<Self>(self).held_toy_;
    }
};

In fact for a easy getter like this, whether or not or not this alteration is price it in your particular use case is as much as you. However for extra advanced features, or circumstances the place you might be coping with giant objects which you wish to keep away from copying, specific object parameters make this a lot simpler to deal with.

CRTP

The Curiously Recurring Template Sample (CRTP) is a type of compile-time polymorphism which lets you prolong sorts with frequent items of performance with out paying the runtime prices of digital features. That is generally known as mixins (this isn’t all the CRTP can be utilized for, however it’s the most typical use). For instance, we may write a sort add_postfix_increment which could be blended in to a different sort so as to outline postfix increment when it comes to prefix increment:

template <typename Derived>
struct add_postfix_increment {
    Derived operator++(int) {
        auto& self = static_cast<Derived&>(*this);

        Derived tmp(self);
        ++self;
        return tmp;
    }
};

struct some_type : add_postfix_increment<some_type> {
    // Prefix increment, which the postfix one is carried out when it comes to
    some_type& operator++();
};

Templating a base class on its derived solid and static_casting this contained in the perform could be a bit arcane, and the issue will get worse when you may have a number of ranges of CRTP. With specific object parameters, since we didn’t change template deduction guidelines, the kind of the express object parameter could be deduced to a derived sort. Extra concretely:

struct base {
    template <class Self>
    void f(this Self&& self);
};

struct derived : base {};

int most important() {
    derived my_derived;
    my_derived.f();
}

Within the name my_derived.f(), the kind of Self inside f is derived&, not base&.

Which means we will outline the above CRTP instance like so:

struct add_postfix_increment {
    template <typename Self>
    auto operator++(this Self&& self, int) {
        auto tmp = self;
        ++self;
        return tmp;
    }
};

struct some_type : add_postfix_increment {
    // Prefix increment, which the postfix one is carried out when it comes to
    some_type& operator++();
};

Word that now add_postfix_increment just isn’t a template. As a substitute, we’ve moved the customisation to the postfix operator++. This implies we don’t have to move some_type as a template argument anyplace: all the things “simply works”.

Forwarding out of lambdas

Copying captured values out of a closure is easy: we will simply move across the object as common. Transferring captured values out of a closure can also be easy: we will simply name std::transfer on it. An issue happens when we have to perfect-forward a captured worth based mostly on whether or not the closure is an lvalue or rvalue.

One use case I stole from P2445 is for lambdas which can be utilized in each “retry” and “attempt or fail” contexts:

auto callback = [m=get_message(), &scheduler]() -> bool {
    return scheduler.submit(m);
};
callback(); // retry(callback)
std::transfer(callback)(); // try-or-fail(rvalue)

The query right here is: how can we ahead m based mostly on the worth class of the closure? Specific object parameters give us the reply. Since a lambda generates a category with an operator() member perform of the given signature, all of the machinary I’ve simply defined works for lambdas too.

auto closure = [](this auto&& self) {
    //can use self contained in the lambda
};

This implies we will perfect-forward based mostly on the worth class of the closure contained in the lambda. P2445 offers a std::forward_like helper, which forwards some expression based mostly on the worth class of one other:

auto callback = [m=get_message(), &scheduler](this auto &&self) -> bool {
    return scheduler.submit(std::forward_like<decltype(self)>(m));
};

Now our unique use case works, and the captured object can be copied or moved relying on how we use the closure.

Recursive lambdas

Since we now have the flexibility to call the closure object in a lambda’s parameter checklist, this permits us to do recursive lambdas! As above:

auto closure = [](this auto&& self) {
    self(); //simply name ourself till the stack overflows
};

There are extra helpful makes use of for this than simply overflowing stacks, although. Take into account, for instance, the flexibility to do visitation of recursive knowledge constructions with out having to outline further sorts or features? Given the next definition of a binary tree:

struct Leaf { };
struct Node;
utilizing Tree = std::variant<Leaf, Node*>;
struct Node {
    Tree left;
    Tree proper;
};

We will rely the variety of leaves like so:

int num_leaves(Tree const& tree) {
    return std::go to(overload( //see beneath
        [](Leaf const&) { return 1; },                       
        [](this auto const& self, Node* n) -> int {              
            return std::go to(self, n->left) + std::go to(self, n->proper); 
        }
    ), tree);
}

overload right here is a few facility to create an overload set from a number of lambdas, and is usually used for variant visitation. See cppreference, for instance.

This counts the variety of leaves within the tree by way of recursion. For every perform name within the name graph, if the present is a Leaf, it returns 1. In any other case, the overloaded closure calls itself by way of self and recurses, including collectively the leaf counts for the left and proper subtrees.

Move this by worth

Since we will outline the qualifiers of the now-explicit object parameter, we will select to take it by worth somewhat than by reference. For small objects, this may give us higher runtime efficiency. In case you’re not aware of how this impacts code era, right here’s an instance.

Say we have now this code, utilizing common outdated implicit object parameters:

See Also

struct just_a_little_guy {
    int how_smol;
    int uwu();
};

int most important() {
    just_a_little_guy tiny_tim{42};
    return tiny_tim.uwu();
}

MSVC generates the next meeting:

sub     rsp, 40                           
lea     rcx, QWORD PTR tiny_tim$[rsp]
mov     DWORD PTR tiny_tim$[rsp], 42     
name    int just_a_little_guy::uwu(void)  
add     rsp, 40                            
ret     0

I’ll stroll by way of this line-by-line.

  • sub rsp, 40 allocates 40 bytes on the stack. That is 4 bytes to carry the int member of tiny_tim, 32 bytes of shadow space for uwu to make use of, and 4 bytes of padding.
  • The lea instruction hundreds the handle of the tiny_tim variable into the rcx register, which is the place uwu is anticipating the implicit object parameter (because of the calling conventions used).
  • The mov shops 42 into the int member of tiny_tim.
  • We then name the uwu perform.
  • Lastly we de-allocate the area we allotted on the stack earlier than and return.

What occurs if we as an alternative specify uwu to take its object parameter by worth, like this?

struct just_a_little_guy {
    int how_smol;
    int uwu(this just_a_little_guy);
};

In that case, the next code is generated:

mov     ecx, 42                           
jmp     static int just_a_little_guy::uwu(this just_a_little_guy) 

We simply transfer 42 into the related register and leap (jmp) to the uwu perform. Since we’re not passing by-reference we don’t have to allocate something on the stack. Since we’re not allocating on the stack we don’t have to de-allocate on the finish of the perform. Since we don’t have to deallocate on the finish of the perform we will simply leap straight to uwu somewhat than leaping there after which again into this perform when it returns, utilizing name.

These are the sorts of optimisations which might forestall “loss of life by a thousand cuts” the place you are taking small efficiency hits again and again and over, leading to slower runtimes which are laborious to search out the foundation reason for.

SFINAE-unfriendly callables

This difficulty is a little more esoteric, however does really occur in actual code (I do know as a result of I received a bug report on my prolonged implementation of std::elective which hit this precise difficulty in manufacturing). Given a member perform of elective known as remodel, which calls the given perform on the saved worth provided that there may be one, the issue seems to be like this:

struct oh_no {
    void non_const();
};

tl::elective<oh_no> o;
o.remodel([](auto&& x) { x.non_const(); }); //doesn't compile

The error which MSVC offers for this seems to be like:

error C2662: ‘void oh_no::non_const(void)’: can not convert ‘this’ pointer from ‘const oh_no’ to ‘oh_no &’

So it’s making an attempt to move a const oh_no because the implicit object parameter to non_const, which doesn’t work. However the place did that const oh_no come from? The reply is contained in the implementation of elective itself. Here’s a intentionally stripped-down model:

template <class T>
struct elective {
    T t;

    template <class F>
    auto remodel(F&& f) -> std::invoke_result_t<F&&, T&>;

    template <class F>
    auto remodel(F&& f) const -> std::invoke_result_t<F&&, const T&&>;
};

These std::invoke_result_ts are there to make remodel SFINAE-friendly. This principally means which you can verify whether or not a name to remodel would compile and, if it wouldn’t, do one thing else as an alternative of simply aborting the whole compilation. Nevertheless, there’s a little bit of a gap within the language right here.

When doing overload decision on remodel, the compiler has to work out which of these two overloads is the most effective match given the forms of the arguments. So as to take action, it has to instantiate the declarations of each the const and non-const overloads. In case you move an invocable to remodel which isn’t itself SFINAE-friendly, and isn’t legitimate for a const certified implicit object (which is the case with my instance) then instantiating the declaration of the const member perform can be a tough compiler error. Oof.

Specific object parameters can help you clear up this downside as a result of the cvref qualifiers are deduced from the expression you name the member perform on: when you by no means name the perform on a const elective then the compiler by no means has to attempt to instantiate that declaration. Given std::copy_cvref_t from P1450:

template <class T>
struct elective {
    T t;

    template <class Self, class F>
    auto remodel(this Self&& self, F&& f) 
    -> std::invoke_result_t<F&&, std::copy_cvref_t<Self, T>>;
};

This permits the above instance to compile whereas nonetheless permitting remodel to be SFINAE-friendly.

Conclusion

I hope this has helped make clear the perform and utility of specific object parameters. You possibly can check out the characteristic in Visual Studio version 17.2. When you have any questions, feedback, or points with the characteristic, you may remark beneath, or attain us through e-mail at visualcpp@microsoft.com or through Twitter at @VisualC.



Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top