Skip to content

[SUGGESTION] Add the equivalent of C++23 "deducing this" #1197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
bluetarpmedia opened this issue Aug 1, 2024 · 15 comments
Open

[SUGGESTION] Add the equivalent of C++23 "deducing this" #1197

bluetarpmedia opened this issue Aug 1, 2024 · 15 comments

Comments

@bluetarpmedia
Copy link
Contributor

For the same motivations as in P0847, I'd like to be able to write the equivalent of the "deducing this" feature in Cpp2.

Here's one example where the feature would prevent duplicating code:

my_optional: <T> type = {
    has_value: bool = false;
    value:     T;

    get: (in this)    -> forward T = { assert(has_value); return value; }  // return const-ref
    get: (inout this) -> forward T = { assert(has_value); return value; }  // return mutable-ref
    get: (move this)  -> move    T = { assert(has_value); return value; }
}

Instead I'd like to write the get function once, with some kind of equivalent "deducing this" syntax that takes care of the this parameter and also the return type, e.g. along the lines of:

get: <Self>(Self _ this) -> _ = { assert(has_value); return value; }

The lowered C++ could duplicate the functions if it's targeting C++20, since the C++23 "deducing this" syntax won't be available.

If, in the future, cppfront supports some kind of cpp_standard flag (like proposed in #942) then the lowered code for C++23 could use the real "deducing this" feature.

Will your feature suggestion eliminate X% of security vulnerabilities of a given kind in current C++ code?
No

Will your feature suggestion automate or eliminate X% of current C++ guidance literature?
Yes, in the same way that "deducing this" does for C++.

@SebastianTroy
Copy link

SebastianTroy commented Aug 1, 2024 via email

@bluetarpmedia
Copy link
Contributor Author

Instead of making special syntax, would get: (forward this) -> forward T = {...} Work instead?

Absolutely, I don't mind what the syntax is, as long as it can lower either to C++23 deducing this or C++20 manually stamping out the variations.

Here's another idea:

get: (deducing this) -> _ = {...}

😆

@dutkalex
Copy link
Contributor

dutkalex commented Aug 2, 2024

@bluetarpmedia Just to be sure: are you saying that we can't use the "deducing this" feature as of now, or are you just advocating for a more direct and explicit syntax to ease the use of the feature?

Btw, I really like your proposed syntax. It doesn't get more explicit than that 😂

get: (deducing this) -> _ = {...}

@bluetarpmedia
Copy link
Contributor Author

Just to be sure: are you saying that we can't use the "deducing this" feature as of now, or are you just advocating for a more direct and explicit syntax to ease the use of the feature?

Currently cppfront lowers to C++20 so to the best of my knowledge there's no way to write Cpp2 that emits the C++23 "deducing this" feature, nor emits the various overloads to simulate it in C++20.

@hsutter
Copy link
Owner

hsutter commented Aug 3, 2024

What we did for "\x{62}" literals was to enable authoring them in Cpp2, but they would only work if your Cpp1 compiler supported them. If we keep doing this there'll need to be some documentation about what's supported and/or possibly a -std= switch to diagnose uses of things not supported in that mode.

@zaucy
Copy link

zaucy commented Aug 3, 2024

I think the syntax for explicit object parameter (deducing this) could simply be used whenever this has an explicit type.

example: (this: Something) -> void = { ... }

lowered to

void example(this Something const& cpp2_self, int i) { ... }

Then any use of this. could lowered to cpp2_self. inside a function using "explicit this". Maybe even require this. for these functions?

As for detection we could simply use the feature-test macro __cpp_explicit_this_parameter at the top of the file if there is any use of "explicit this".

@AbhinavK00
Copy link

What I have is more of a question. As far as I understand, the deduplication of cpp code due to deducing this is because we can do perfect forwarding with the 'this' parameter.
In cpp2, we use the 'forward' passing convention to do forwarding. So why does the following not work/what is the difference in behavior

get: (forward this) -> forward T = {...}

@hsutter
Copy link
Owner

hsutter commented Aug 3, 2024

Thanks for the suggestions!

For a this parameter, Cpp2 currently disallows exactly (a) forward and (b) an explicit type... and yes, it sure does seem like that may be exactly the design space needed for deducing this? That's quite a convergence! -- I didn't pay close attention to the deducing-this feature's progress, and didn't design the Cpp2 restrictions on this with it in mind.

It's easy enough to still allow code in the function body to consistently write this to refer to the parameter, even if we emit it as this_ or self under the covers to satisfy Cpp1 requirements in C++23.

Here's an adaptation of the first two examples from cppreference, which I think covers the core use cases...

// C++23
//
struct X {
    template<typename Self>
    void foo(this Self&& self, int) {
        do_something_with( self );
    }
};

struct D : X {};
 
void ex(X& x, D& d) {
    x.foo(1);       // Self = X&
    move(x).foo(2); // Self = X
    d.foo(3);       // Self = D&
}

Note that Cpp2 already makes forward parameters be implicit templates, so something like this seems natural? In addition, unlike Cpp1, this would also std::forward<Self>(self) from the last use to preserve the cv-qualification and value category of this object.

// Possible Cpp2?
//
X: @struct type = {
    foo: (forward this) = {
        do_something_with( this );
    }
};

D: type = {
    this: X;
}

ex: (inout x: X, inout d: D) = {
    x.foo(1);       // typeof(this) = X&
    move(x).foo(2); // typeof(this) = X
    d.foo(3);       // typeof(this) = D&
}

Something like that, perhaps?

@hsutter
Copy link
Owner

hsutter commented Aug 3, 2024

The above would just allow forward this to have this meaning, and doesn't yet allow actually specifying a type. Are there any examples not covered by the above that would also require specifying a type for a this parameter?

Update to answer my own question: Yes, forward this: specific_type would be a reasonable thing to do, and that type should be limited to being the enclosing type's name, I think.

@bluetarpmedia
Copy link
Contributor Author

This (no pun intended) sounds promising! One thing I'm wondering about is a function that returns auto&& in C++. For example, how would we write the Cpp2 to emit the following example from Sy Brand's blog post?

template <class Self>
constexpr auto&& value(this Self&& self) {
    if (self.has_value()) {
        return std::forward<Self>(self).m_value;
    }
    throw bad_optional_access();  // Ignore this part
}

I think this function signature would work for constexpr and returning auto&&:

value: (forward this) -> forward _ == { ... }

But how would we write the equivalent for this line?

return std::forward<Self>(self).m_value;

Would it be the following?

return this.m_value;

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 4, 2024

The above would just allow forward this to have this meaning, and doesn't yet allow actually specifying a type. Are there any examples not covered by the above that would also require specifying a type for a this parameter?

Yes.
It can be useful to use pass the (possibly derived) deduced type to std::forward_like.
The paper, P0847, has an example.

template <typename F>
auto not_fn(F&& f) {
    return [f=forward<F>(f)](this auto&& self, auto&&.. args)
        BOOST_HOF_RETURNS(
            !invoke(
                forward_like<decltype(self)>(f),
                forward<decltype(args)>(args)...))
        ;
}
not_fn: (forward f) =
  :<Self> (forward this: Self, forward args...) =
    BOOST_HOF_RETURNS(!invoke(forward_like<Self>(f$), args...));

The main branch already knows that the type-id Self names a template parameter,
so it can known this is in fact a forwarding parameter.
Although that doesn't seem to be currently rejected (see https://cpp2.godbolt.org/z/vr63W6e46).

Update to answer my own question: Yes, forward this: specific_type would be a reasonable thing to do, and that type should be limited to being the enclosing type's name, I think.

This is very similar to #572 (comment).
It seems like deducing this already allows you to place this's type: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html#sfinae-friendly-callables.
There might be a conflict by using forward this for deducing this if Cpp1 gets the other feature.

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 4, 2024

I've always wanted this.
I first mentioned it at 4bd0c04#commitcomment-133307333.
The second comment has an implementation that errors when deducing this isn't supported (on use of this).

A quirk with a deducing this Cpp2 forward this parameter
is that, suddenly, you need to this.-qualify uses of members.
The need isn't as apparent in the Cpp2 syntax as in the Cpp1 syntax.
Is it the lack of an explicit name like in Cpp1 (conventionally self)?

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 4, 2024

Deducing this also allows declaring the object parameter by-copy.
From https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html#by-value-member-functions-for-performance:

template <class charT, class traits = char_traits<charT>>
class basic_string_view {
private:
    const_pointer data_;
    size_type size_;
public:
    constexpr const_iterator begin(this basic_string_view self) {
        return self.data_;
    }

    constexpr const_iterator end(this basic_string_view self) {
        return self.data_ + self.size_;
    }

    constexpr size_t size(this basic_string_view self) {
        return self.size_;
    }

    constexpr const_reference operator[](this basic_string_view self, size_type pos) {
        return self.data_[pos];
    }
};

We could also allow a copy this parameter to mean that by lowering to Cpp1 this current_type self.
But what if you also want the forward this semantics above?
Do we add forward_copy for that?

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 4, 2024

Update to answer my own question: Yes, forward this: specific_type would be a reasonable thing to do, and that type should be limited to being the enclosing type's name, I think.

This is very similar to #572 (comment).
It seems like deducing this already allows you to place this's type: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html#sfinae-friendly-callables.
There might be a conflict by using forward this for deducing this if Cpp1 gets the other feature.

Let's be more clear.

#572 (comment) says that the current main branch implements from P2481:

  • the fully generic part with f: (forward x: _) = { ... }, and
  • the type_of constrained part with f: (forward x: of_my_type) = { ... }.

Today's f: (forward x: of_my_type) = { ... } only requires convertibility to of_my_type.
The part of P2481 cppfront doesn't (yet) implement is making x be of_my_type.
Including f: (forward x: std::variant) = { ... } deducing from derived types but converting to the std::variant base.

For P0847 (deducing this, explicit object member functions),
Herb proposes the natural spelling of mem: (forward this).

Now, where both of these features would meet is in mem: (forward this: of_my_type).
It seems to me that both P2481 and P0847 would give it the same meaning.
P2481 would make this's type be of_my_type, deducing constness and reference, accepting derived types.
P0847 would do the same, and an explicit object parameters accepts derived type conversions as usual.

If I'm analyzing this right, these features are not actually in conflict.
So sharing the same syntax (i.e., when a forward parameter is for this) should be fine.

@JohelEGP
Copy link
Contributor

JohelEGP commented Oct 5, 2024

We could also allow a copy this parameter to mean that by lowering to Cpp1 this current_type self.
But what if you also want the forward this semantics above?
Do we add forward_copy for that?

Actually, forward_copy doesn't make sense.

But both copy and forward this parameters share behavior from P0847.

  • They need to be implemented with Cpp1 explicit object parameters.
    • Their use will require explicit this. qualification.
  • They need to allow specifying a type other than current_type.
  • f: (copy this) = { ... } should be made to work.
  • f: (copy this: derived_type) = { ... } should be made to work (https://cpp2.godbolt.org/z/oMjPbv3ro).
  • f: (forward this) = { ... } should be made to work (forwarding parameter to current_type).
  • f: (forward this: _) = { ... } should be made to work (generic forwarding parameter).

Additionally, from P2481:

  • f: (forward this-or-name: X) = { ... } should be made to work, coercing the argument to an X.

What we already have from P2481 (as Cpp2 features):

  • f: (forward name: _) = { ... } itself.
  • f: (forward name: specific_type) = { ... } as something close to
    f: (forward name: _ is std::convertible_to<specific_type>) = { ... }.
    Although I'm wary of how it's currently implemented.

Combining these two features, we get:

  • f: (forward this: derived_type) = { ... }.

JohelEGP referenced this issue Oct 11, 2024
Requiring `=` has been a widely-requested change, and I think it does make the code clearer to read. See the Cpp2 code changes in this commit for examples.

For example, `f: () expr;` must now be written as `f: () = expr;`

For a function whose body is a single expression, the default return type (i.e., if not specified) is now `-> forward _`, which is Cpp1 `-> decltype(auto)` (which, importantly, can deduce a value).

With this change, single-expression function bodies without `{ }` are still legal for any function, but as of this commit we have a clearer distinction in their use (which is reflected in the updates to the regression tests and other Cpp2 code in this commit):

    - It further encourages single-expression function bodies without `{ }` for unnamed function expressions (lambdas), by making more of those cases Do the Right Thing that the programmer intended.

    - It naturally discourages their overuse for named functions, because it will more often cause a compiler warning or error:

       - a warning that callers are not using a returned results, when the function is called without using its value

       - an error that a deduced return type makes the function order-dependent, when the function is called from earlier in the source file... this is because a deduced return types creates a dependency on the body, and it is inherent (not an artifact of either Cpp1 or Cpp2)

       But for those who liked that style, fear not, usually the answer is to just put the two characters `{ }` around the body... see examples in this commit, which I have to admit most are probably more readable even though I'm one of the ones who liked omitting the braces.

       Note: I realize that further evolution may not allow the `{ }`-free style for named functions. That may well be a reasonable further outcome.

I think both of these are positive improvements.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants