Skip to content
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

[BUG] Need decltype(auto) for perfect backwarding #876

Closed
JohelEGP opened this issue Dec 6, 2023 · 31 comments
Closed

[BUG] Need decltype(auto) for perfect backwarding #876

JohelEGP opened this issue Dec 6, 2023 · 31 comments
Labels
bug Something isn't working

Comments

@JohelEGP
Copy link
Contributor

JohelEGP commented Dec 6, 2023

Title: Need decltype(auto) for perfect backwarding.

Description:

At first, a Cpp2 -> forward _ return-list lowered to the Cpp1 return type auto&&.

#175 noted this was unsafe,
and commit b59f539 changed it to decltype(auto).

#274 noted that this makes accessors return by copy,
then commit 413de0e changed it back to auto&&,
and commit 43cdf3e addressed some safety concerns of #175.

Now perfect backwarding doesn't work or returns a temporary.
This means we can't implement std::invoke or simpler equivalents.
For example, implementing a simple function_ref: https://cpp2.godbolt.org/z/r53zfnxbT.

Minimal reproducer (https://cpp2.godbolt.org/z/bjjfqEc9e):

f: (x: int) x;
f: (x: long) _ = x;
g: (x) -> forward _ = f(x);
main: () = {
  _ = g(0);
  _ = g(0L);
}
Commands:
cppfront main.cpp2
clang++18 -std=c++23 -stdlib=libc++ -lc++abi -pedantic-errors -Wall -Wextra -Wconversion -Werror=unused-result -Werror=unused-value -Werror=unused-parameter -I . main.cpp

Expected result: Perfect backwarding to work.

Actual result and error:

Cpp2 lowered to Cpp1:
//=== Cpp2 type declarations ====================================================


#include "cpp2util.h"

#line 1 "/app/example.cpp2"


//=== Cpp2 type definitions and function declarations ===========================

#line 1 "/app/example.cpp2"
[[nodiscard]] auto f(cpp2::in<int> x) -> auto;
#line 2 "/app/example.cpp2"
[[nodiscard]] auto f(cpp2::in<long> x) -> auto;
[[nodiscard]] auto g(auto const& x) -> auto&&;
auto main() -> int;

//=== Cpp2 function definitions =================================================

#line 1 "/app/example.cpp2"
[[nodiscard]] auto f(cpp2::in<int> x) -> auto { return x;  }
#line 2 "/app/example.cpp2"
[[nodiscard]] auto f(cpp2::in<long> x) -> auto { return static_cast<void>(x);  }
[[nodiscard]] auto g(auto const& x) -> auto&& { return f(x);  }
auto main() -> int{
  static_cast<void>(g(0));
  static_cast<void>(g(0L));
}
Output:
main.cpp2:3:56: warning: returning reference to local temporary object [-Wreturn-stack-address]
    3 | [[nodiscard]] auto g(auto const& x) -> auto&& { return f(x);  }
      |                                                        ^~~~
main.cpp2:5:21: note: in instantiation of function template specialization 'g<int>' requested here
    5 |   static_cast<void>(g(0));
      |                     ^
main.cpp2:3:44: error: cannot form a reference to 'void'
    3 | [[nodiscard]] auto g(auto const& x) -> auto&& { return f(x);  }
      |                                            ^
main.cpp2:6:21: note: in instantiation of function template specialization 'g<long>' requested here
    6 |   static_cast<void>(g(0L));
      |                     ^
1 warning and 1 error generated.

See also:

@JohelEGP JohelEGP added the bug Something isn't working label Dec 6, 2023
@JohelEGP
Copy link
Contributor Author

JohelEGP commented Dec 6, 2023

An alternative is to change back to decltype(auto)
and lower returned member names in parentheses.
That should make -> forward _ return-list "work as expected" more.
The relevant features are https://en.cppreference.com/w/cpp/language/auto and https://en.cppreference.com/w/cpp/language/decltype.

@JohelEGP
Copy link
Contributor Author

Yet another alternative is to change back to decltype(auto),
but reject returning unparenthesized member names.
They should be (member) or (copy member) (#466 (comment)).

@hsutter
Copy link
Owner

hsutter commented Sep 25, 2024

Moving the comment thread from #921 here:

Thanks! Sorry for the lag.

It seems like the motivation overlaps with the later discussion in #714 which starts to add decltype(auto), though only for single-expression bodies.

Perhaps this and #876 are underscoring that there need to be three (not two) return styles, which has come up in my mind several times before:

  • by value, which is currently default or can be explicitly spelled move... maybe this should be spelled copy?
  • by reference to support string::operator[] use cases, which is currently spelled forward... maybe this should be spelled something like ref (*)?
  • by perfect backwarding, which currently has no spelling but the natural name would be forward and lower to decltype(auto)?

What do you think? That would bottom out on both #714 and #876, and be instead of this PR?


(*) There's a longer story here: For many years, the best candidate names I've considered for "in/out" parameters are inout and ref.

  • ref has the advantage of symmetry: The same word could be directly used for both parameters and returns that are lvalue references. However, a major disadvantage is that it's about "how" to pass the parameter, not "what" the parameter is for.
  • inout has the (IMO compelling) advantage of "what" not "how": Plus it connotes explicit data-flow. It's just perfect for parameters IMO. However, inout doesn't feel quite right as a return from string::operator[].

I liked the idea of trying to make the return styles be a subset of the parameter styles, but maybe it's time to reconsider that it doesn't quite work. If we keep inout for lvalue reference parameters, what's the right "what" word for what string::operator[] returns... what is a "what" word that describes what the callee/caller wants to do with it?

Brainstorming... loan? lend? show? share? actual? original? or maybe qualifier_forward?

Naming is hard.


So why not continue using concrete -> forward if it's still a thing?

The question is that just as for parameters there's a distinction between inout (pass by reference) and forward parameters (deduce and remember the argument's const-ness and rvalue-ness), for return values you're pointing out in #876 there's a distinction in return values between what string::operator[] wants to do (return by reference) and what -> decltype(auto) wants to do (remember the argument's characteristics).

I suspect that these two were conflated instead of being supported separately might have been a contributing factor to the back-and-forth about the -> forward lowering mentioned at top.


#714's gotten long and contentious (I'm a fan of the main branch's :(x) x standing for :(x) -> _ = x).
Can I have some specific links to comments?

I think this was my last strawman: #714 (comment)

If we add a third return style I'll take another pass over the table.

@JohelEGP
Copy link
Contributor Author

So why not continue using concrete -> forward if it's still a thing?

The question is that just as for parameters there's a distinction between inout (pass by reference) and forward parameters (deduce and remember the argument's const-ness and rvalue-ness), for return values you're pointing out in #876 there's a distinction in return values between what string::operator[] wants to do (return by reference) and what -> decltype(auto) wants to do (remember the argument's characteristics).

I suspect that these two were conflated instead of being supported separately might have been a contributing factor to the back-and-forth about the -> forward lowering mentioned at top.

I think I understand the problem.

We have:

  • inout for a by reference parameter (including to const since commit f44dda9),
  • forward for a deduced parameter (with conversion requirements since commit 36c5a9e),
  • -> forward T to return T by reference, and
  • -> forward _ for a deduced return (that doesn't work for perfect backwarding).
  1. You think that -> forward is the natural spelling for perfect backwarding.
  2. We need a spelling for return by reference.

Reusing inout, string's operator[]: (inout this, index) -> inout char; does say the "what".
What the caller can do with the return value is read from it (isn't it obvious?) and write to it.

@hsutter
Copy link
Owner

hsutter commented Sep 26, 2024

Thanks -- the way you phrased this is important:

Reusing inout, string's operator[]: (inout this, index) -> inout char; does say the "what".
What the caller can do with the return value is read from it (isn't it obvious?) and write to it.

You might have just put your finger on something I was missing about inout for returns.

I was fixated on there being no "in" data flow part in a return, so that "in" didn't make sense, and always hit a wall with that. But when you state it as above, now I see maybe it is symmetric after all.

Let me try to phrase it the way you did:

  • An inout parameter expresses "give me [the callee] an object I can read and write." That naturally requires lvalue arguments, otherwise the "out" data flow would be lost by default for rvalue arguments such as temporaries whose lifetimes would end at the end of the full call-expression and so their 'out' values would be lost.
  • An inout return (we could say) expresses "give you [the caller] an object you can read and write." That also naturally requires lvalue returns, otherwise it would be a lifetime error to return an rvalue (and we have some, and will add more, language/analysis checks for that anyway).

How does that sound?

Then we have for parameters (quoting the docs, which I just tweaked to emphasize it's about what "the function" can do):

Parameter kind "Pass an x the function ______" Accepts arguments that are Special semantics kind x: X Compiles to Cpp1 as
in (default) can read from anything always #!cpp const

automatically passes by value if cheaply copyable

X const x or X const& x
copy gets a copy of anything acts like a normal local variable initialized with the argument X x
inout can read from and write to lvalues X& x
out writes to (including construct) lvalues (including uninitialized) must = assign/construct before other uses cpp2::impl::out<X>
move moves from (consume the value of) rvalues automatically moves from every definite last use X&&
forward forwards anything automatically forwards from every definite last use T&& constrained to type X

And the updated model and description for return values could be a subset that includes inout after all:

Return kind "Return an x the caller ______" Accepts returns that are Special semantics -> kind X Compiles to Cpp1 as
copy gets a copy of anything acts like a normal returned object initialized with the return expression -> X
inout can read from and write to lvalues -> X&
forward forwards anything -> decltype(auto) constrained to type X

What do you think?

I need to sleep on it, but this may mean inout is a fine "what" word for (single anonymous) returns after all.

@AbhinavK00
Copy link

For string's operator[] like use cases, there are usually two overloads, one that returns a const reference and one that returns a mutating reference.
So there should also be a return kind to signal returning a const reference. Is that correct or this case is already handled by the above 3 kinds?

@JohelEGP
Copy link
Contributor Author

  • inout for a by reference parameter (including to const since commit f44dda9),

We have inout: const parameters to alias arguments as read-only,
as opposed to in parameters, which might make a copy, and thus not alias.
Where today we have to write front: (this) -> forward const char = buffer[0];,
we could similarly extend to front: (this) -> inout const char = buffer[0];.
That indicates that the return aliases (even though there's no in return anyways).

@AbhinavK00
Copy link

I thought that was spelled as in x : const T instead of inout x : const T . But anyways, inout specifies the flow of information both in and out of the function which makes sense for returning mutating references but returning a const reference means information is only going one way.

What the caller can do with the return value is read from it (isn't it obvious?) and write to it.

Complementary to the above reasoning, the caller can only read from it.

@hsutter
Copy link
Owner

hsutter commented Sep 26, 2024

I thought that was spelled as in x : const T

in implies const.

  • inout for a by reference parameter (including to const since commit f44dda9),

We have inout: const parameters to alias arguments as read-only,

We do have that, and it would mean the same for return values and thus directly address the string [] const overload problem with -> inout const.

However, combining inout and const has always bugged me a little. But I tried the experiment because didn't have an answer I was sure was better, and the inout: const use cases seemed to be pretty rare (like std::min). I wonder whether use cases like string [] (and as you point out front, and back and iterator operator* etc.) may be more common though.

I've been resisting changing inout to ref because I like the inout "what" and data-flow semantics, and ref breaks that consistency and still feels like a "how". But ref: const does feel cleaner than inout: const.

Strawman 1: Musing aloud about ref...

If you look at the first triplet of the list { in, inout, out, copy, move, forward }, there's a clear pattern and symmetry argument for { in, inout, out }. However, more counterarguments against inout are:

  • the first is the default so you usually never actually write it, and so the list is in practice arguably { whitespace, inout, out, copy, move, forward } which has less advantage over { whitespace, ref, out, copy, move, forward }
  • the proposed subset list for returns is inout, copy, forward which has less advantage over ref, copy, forward
  • perhaps ref could after all be viewed as a "what" (not "how") if viewed as "Pass an x the function can refer to the original argument"
  • copy already could arguably be viewed as a "how" not a "what"

So if we renamed inout to ref, then we would have:

// Parameters
min: (ref a: const _, ref b: const _) -> ref const _ = { /*...*/ }
inc: (ref a) = a++;

// Returns for overloaded front (etc. for back, operator[], ...)
front: (/*in*/ this) -> ref const char = buffer[0];
front: (ref    this) -> ref       char = buffer[0];

That gives some improvement, but it doesn't always put "what" first, and it still puts some of "what" after the colon, so it still doesn't seem fully clean.

But it leads to another thoughts... maybe there's a way to do better via a refactoring, and actually factor out "copy" and "ref":

Strawman 2: Further musing about in_*

Another way to look at is from the point of view of in, which by default copies or binds depending on the type, which I think is a great feature and is the right default. However, experience has consistently shown that people do sometimes need to know and control when in copies and when it binds, and to force one outcome or the other, and so we invented copy and inout: const...

But if instead we viewed them as specializations of in, another option would be to rename copy and inout: const to in_copy and in_ref. That preserves the key feature that the "in" dataflow is the most important "what" that always comes first (by syntactically demoting the "how"-like copy, and consistently including implicit const), and would be embracing the view that "the programmer can always open the hood" to distinguish the two "hows" that are by default automated by in, for when you really want to take control.

So if we renamed inout: const to in_ref, then we would have:

// Parameters
min: (in_ref a, in_ref b) -> in_ref _ = { /*...*/ }
inc: (inout a) = a++;

// Returns for overloaded front (etc. for back, operator[], ...)
front: (/*in_ref*/ this) -> in_ref char = buffer[0];
front: (inout      this) -> inout  char = buffer[0];

This would effectively mean:

  • reducing the list to my original five parameter passing styles, { in, inout, out, move, forward }
  • adding two 'advanced' options for the first one, in_copy and in_ref (which also lets us remove the inout: const wart)

Perhaps that's cleanest... and perhaps the strategy extends well to returns:

Also: Cpp1 -> decltype(auto) and -> auto&&

As Johel pointed out at the very top in this issue description, part of the reason for the switching back and forth on -> forward was because of switching between these two meanings, which is a tension if there is only one spelling and it has to mean only one of those two things.

Interestingly, I think I see a symmetry here with in: Like in which can take by value or reference, so also Cpp1 -> decltype(auto) returns by value or by reference, whereas Cpp1 -> auto&& is always a reference. I think that's the main difference.

So Strawman 2's approach of a _copy or _ref suffix to in gives a consistent path to deal with that by adding the same option on the return side: Cpp1 -> decltype(auto) and -> auto&& could be spelled as -> forward _ and -> forward_ref _. (Which leaves room for -> forward_copy if there are motivating use cases.)

And then we would have some symmetry:

  • back to the five original styles, { in, inout, out, move, forward }, two of which can let you orthogonally take control with _copy or _ref
  • returns being a subset, { inout, move, forward/*_ref*/ } (I think I still prefer the status quo move for value return because it emphasizes that move will happen by default wherever possible as is already true for Cpp1 value returns, whereas "copy" may not always happen so that word could actually be untrue).

End of morning musings... I'll keep thinking about it, but I suspect we're on the cusp of making a final adjustment to parameter passing (such as here) and the the function call syntax (use = everywhere), and doing them in tandem is appropriate because the former is referenced by the latter. Pasting, essentially the same as the last in #714 comment thread, but changing "copyable" to "movable" and noting the -> decltype(auto) meaning:

image

What do you think?

Does Strawman 2 extended to returns address the cases of:

  • return by reference (e.g., string types)
  • the back-and-forth tension on whether -> forward should emit auto&& vs decltype(auto)
  • putting "what" data-flow first
  • not needlessly complicating the declarative pass/return styles

?

@DyXel
Copy link
Contributor

DyXel commented Sep 26, 2024

ref is not that bad when you consider the existence of type traits such as remove_cvref and more.

@JohelEGP
Copy link
Contributor Author

I like it, but...

And then we would have some symmetry:

  • back to the five original styles, { in, inout, out, move, forward }, two of which can let you orthogonally take control with _copy or _ref

One is in_copy.
Which is the other is?
It seems like we lost today's copy for the callee to have a copy of the argument they can write to.

@gregmarr
Copy link
Contributor

Quick thought about making it more obvious that these are modifiers to the base behavior, and just modify the method of passing and that it doesn't modify that you get an implicitly const parameter: in<copy>, in<ref>, (read as "in by copy" and "in by reference") where the current behavior is in<auto> which can be shortened to in. Maybe in<value> (read as "in by value") to differentiate it with the current copy which gives you a modifiable copy. Not sure if the benefit is worth this much syntax complication.

@AbhinavK00
Copy link

in implies const

The title of that commit said in x : const T so that's what I thought was allowed, it was probably changed after the discussion.

One way could be to just spell returning by const references as in and teach that the optimization happens only for arguments, not for returns. That could work, it's an optimization after all.

But I feel cpp2 is just reinventing references all over again with different spelling. The improvement that the original paper promised has now diminished to just names. Maybe this is a good time to rethink the parameter kinds from ground up now that there's plenty of experience of using them?

@gregmarr
Copy link
Contributor

But I feel cpp2 is just reinventing references all over again with different spelling. The improvement that the original paper promised has now diminished to just names. Maybe this is a good time to rethink the parameter kinds from ground up now that there's plenty of experience of using them?

The original premise was that references were intended only for parameter passing and return types, and so keeping them in parameter passing and return types with different names still aligns with that, I think.

@AbhinavK00
Copy link

The original premise was that references were intended only for parameter passing and return types

Cpp2 could do that by simply rejecting references outside of parameter lists.
Don't get me wrong, parameter kinds are great and simply giving names which convey intent goes a long way in simplifying how cpp2 will be taught. But I just wish that cpp2 ends up with less passing conventions than in C++.
Currently cpp2 has 1:1 parameter kind for every parameter passing style in C++, except for const rvalue references, plus two more in form of out and in. It would be great if that number could be reduced.

@gregmarr
Copy link
Contributor

gregmarr commented Sep 26, 2024

@hsutter Since there are not supposed to be local references, what is your expectation for how we use the vector::operator[] return value? Can we only use it immediately such as vec[i].DoSomething(); or via structured bindings?

Cpp2 could do that by simply rejecting references outside of parameter lists.

Yes, but that doesn't make them any more learnable.

Don't get me wrong, parameter kinds are great and simply giving names which convey intent goes a long way in simplifying how cpp2 will be taught. But I just wish that cpp2 ends up with less passing conventions than in C++.
Currently cpp2 has 1:1 parameter kind for every parameter passing style in C++, except for const rvalue references, plus two more in form of out and in. It would be great if that number could be reduced.

If you reduce the number of kinds, you reduce the expressiveness and capability, unless there are redundant ones in cpp1 that can be merged (which happened), or ones in cpp1 that should never be used and are removed (which also happened).

The in is "const lvalue references with the optimization that it will pass by const value if it's a fundamental type that fits in two pointers" so that merges two cpp1 styles. It also eliminates slicing of parameters by not accepting user defined types by value.

The addition of out is for initialization safety, so it's a new capability on lvalue reference. Therefore, it needs a new form.

The removal of const rvalue references is intentional, as they are redundant with const lvalue references.

cpp1 has 7 styles if you consider value and const value as different (they are different on the callee side, not on the caller side): value, const value, lvalue reference, const lvalue reference, rvalue reference, const rvalue reference, universal reference

cpp2 has 6 styles
in: const value or const lvalue reference
inout: lvalue reference
out: lvalue reference with mandatory initialization before use (new capability)
move: rvalue reference
copy: non-const value (@hsutter this can not currently prevent slicing of user-defined types, would be good to explore this)
forward: universal reference

@AbhinavK00
Copy link

cpp2 has 6 styles

There's
in
inout : const
inout : /*non const*/
copy : const
copy : /*non-const*/
out
forward
move

If this comment is implemented, there will be

in
in_copy
in_ref
inout : /*non const*/ (notice that const version goes away)
copy : const (this one should also go away as it is already covered by in_copy)
copy : /*non-const*/
out
move
forward
forward_ref

I'm not against in or out in any way, I think they are great, especially out since it helps with initialization safety. I also can't think of a way to remove any of the above, but the current amount is too much compared to other languages.

@MaxSagebaum
Copy link
Contributor

I really like the idea of in_copy and in_ref. The suggestion from @gregmarr of in<copy> looks also nice. But for tooling it will be simpler to have exact keywords like in_copy. So I think I would for the underscore. :)

@abhinav00 I would be also in favor of reducing the number of passing and return kinds. If Cpp2 would be a new language, this could be done without any problems. Since we need to be compatible with Cpp1 and transpile to Cpp1 there is probably no way around it.

@gregmarr
Copy link
Contributor

gregmarr commented Sep 27, 2024

I was basing my count of 6 on this, plus restoring copy which isn't covered because it's explicitly non-const, which I thought would be needed.

back to the five original styles, { in, inout, out, move, forward }, two of which can let you orthogonally take control with _copy or _ref

But if instead we viewed them as specializations of in, another option would be to rename copy and inout: const to in_copy and in_ref. That preserves the key feature that the "in" dataflow is the most important "what" that always comes first (by syntactically demoting the "how"-like copy, and consistently including implicit const), and would be embracing the view that "the programmer can always open the hood" to distinguish the two "hows" that are by default automated by in, for when you really want to take control.

in (in_copy + in_ref or in<copy> + in<ref> for advanced usage)
inout
copy
out
move
forward

The forward_ref and perhaps forward_copy are solely return styles.

@hsutter
Copy link
Owner

hsutter commented Sep 29, 2024

Interim mea culpa: Ignore most of what I wrote above, which is confusing the issue because I just forgot about problems that have already been solved.

For example, no we don't need inout returns for the string::front/back/[] example, because they're spelled forward specific_type:

// This was already covered, not a problem
// In a 'string' type:
front: (/*in*/ this) -> forward char = buffer[0];  // implicitly 'forward const char'
front: (inout  this) -> forward char = buffer[0];

So we already solved most of what I wrote above. The main thing I don't think is yet addressed is the deduced return type being able to be a value or not.

Sorry for the noise. More (and simpler) update soon...

@AbhinavK00
Copy link

Small suggestion, just use decltype(_) to spell decltype(auto).

@hsutter
Copy link
Owner

hsutter commented Oct 3, 2024

Catching up:

Re do we need to replicate Cpp1 styles: No, never. We just need to cover the use cases using "what" not "how. The -> decltype(auto) and -> auto&& distinction needs to be supported in Cpp2 not because Cpp1 has those two things, but rather because deduced return with/without value return being an option is a valid use case. (*)

Plan

I think I've got an improved simple answer:

Style Parameter Return
in
inout
out
copy
move
forward

The two ⭐'s are the cases that automatically pass/return by value or reference:

  • all uses of in
  • deduced uses of -> forward _ (ordinary -> forward specific_type always returns by reference, adding constness if there's an in this parameter)

So both those should also provide a _ref option to not choose by-value. I think that reasonably addresses the fit-and-finish issues I know of with parameter passing:

  • the odd inout : const becomes just in_ref
  • we directly express both Cpp1 -> decltype(auto) and -> auto&& as Cpp2 -> forward _ and -> forward_ref _

(*) And this is still way simpler than the Cpp1 styles which cover way more than the use cases. Today in Cpp1 we have to teach (among other problems):

  • "don't do" various combinations (e.g., C const&&)
  • "which to use" for multiple divergent syntaxes to do the same thing (e.g., T and T const&,)
  • "which is intended" for single syntaxes that do multiple divergent things (e.g., T& could be an inout or out parameter, T* could be an in _: *T pointer or an out _: T, T&& could be a move or forward depending on whether T is a dependent name)

@gregmarr
Copy link
Contributor

gregmarr commented Oct 3, 2024

@hsutter I like that description.

Another quick pass on bikeshedding of in_copy, in_ref and forward_ref. I know we want to keep them short, and the in<copy> is probably too heavy of a syntax, but would something like in_byvalue in_byref and forward_asref (or forward_byref) make it clearer that these are modifiers to the base type?

There is also the question of what we should use on the calling side to receive this forward_asref return value? Are we limited to either immediately chaining or taking the address to avoid then making a copy?

@gregmarr
Copy link
Contributor

gregmarr commented Oct 3, 2024

Interesting variation in how Herb's table appears on GitHub in Chrome on Mac (bottom) vs in the email notification in Outlook on Mac (top).
image

@JohelEGP

This comment was marked as resolved.

@hsutter
Copy link
Owner

hsutter commented Oct 3, 2024

That's because I edited the check/star symbols after initially posting... you're right, the original way was better 👍
Now changed back.

@hsutter
Copy link
Owner

hsutter commented Oct 3, 2024

the in<copy> is probably too heavy of a syntax

Note I decided not to do an in_copy... it would have replaced copy, but after thinking about it I decided that copy really was a distinct "what" because it implied "give me an X I can use as a local variable" => non-const. Having an in_copy that was still const seemed like not a useful addition if we already have (and keep) copy.

So _ref is the only qualifier needed, and only for those cases that might deduce a non-ref (value). I like that a lot, it's clean.

@gregmarr
Copy link
Contributor

gregmarr commented Oct 3, 2024

Note I decided not to do an in_copy... it would have replaced copy
Having an in_copy that was still const seemed like not a useful addition

I thought when I was posting last week that I had a use case for in_copy being const and separate from copy, where you wanted the in semantic of a const value, but for some reason wanted to prevent pass by reference. The only thing I can come up with now is that you want your own immutable copy so that you are immune from any outside modifications.

One benefit of in that I noticed and mentioned earlier is that you can't slice user defined types, because they will always be by reference, so maybe in_copy is actually something that you actively don't want.

That then made me realize that copy DOES allow slicing of user defined types if it's just T, so would it be worth investigating adding something that would make it an error to call it with a derived type that adds data members? I briefly looked at making a template<typename T> copy to mimic template<typename T> in but didn't get far enough into it to see if it would be usable, if I could actually make it not accept derived types of a larger size.

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Oct 3, 2024

@hsutter
Copy link
Owner

hsutter commented Oct 3, 2024

you want your own immutable copy so that you are immune from any outside modifications

Yes, and you'll still be able to write f: (copy a: const A) which lowers to Cpp1 auto f(A const a) -> void. A copy parameter is also a local variable, and those can be declared const.

@hsutter hsutter closed this as completed in 54d9b9b Oct 4, 2024
@gregmarr
Copy link
Contributor

gregmarr commented Oct 4, 2024

Yes, and you'll still be able to write f: (copy a: const A)

Makes sense.

According to the guidelines, you shouldn't be copying in the first place.

Yes, and it would be good to provide tools to make sure users don't copy inappropriately. We are preventing it with in, it would be good to do it with copy too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

6 participants