Skip to content

[QUESTION] How to make of inout parameters of type const T? #250

Closed

Description

@leejy12

I was thinking of how Cpp2 would implement std::min. Its signature looks like this:

template <typename T>
const T& min(const T& a, const T& b);

I came up with this answer (the template syntax is copied from #248 (comment):

min: <T: type> (inout a: const T, inout b: const T) -> forward const T = {
    if a < b {
        return a;
    }
    else {
        return b;
    }
}

https://godbolt.org/z/8Wa315arP

However, this raises some questions about inout and const parameters.

inout x: X means "present me an X I can read and write" and the function must contain a non-const use of X. But functions like std::min can't have write operations to its parameters because they're const reference. So cppfront should not give error diagnostics when there's no write operations to parameters that are inout and const.

I think this slight conflict of intentions could be confusing to beginners learning the language. Unless there's a better way, we could treat this as a special case of using inout and const for functions that MUST take parameters by const reference. I don't think there's many functions that do, I can only think of std::min, std::max, and std::clamp.

Activity

  1. SebastianTroy commented on Feb 7, 2023

    @SebastianTroy
  2. AbhinavK00 commented on Feb 7, 2023

    @AbhinavK00

    Taking in by a pointer in this case looks like a work-around, we probably need a new way for this use case.

    Off topic but I think template syntax could just have
    func : <T , U> ()= instead of func : <T : type, U : type> so that template arguments can default to typename and the function writer can specify otherwise.

  3. filipsajdak commented on Feb 7, 2023

    @filipsajdak
    Contributor

    @leejy12, what you try to do is probably:

    min: (in a, in b) -> forward _ = {
        if a < b {
            return a;
        }
        else {
            return b;
        }
    }

    That will generate the following code:

    [[nodiscard]] auto min(auto const& a, auto const& b) -> decltype(auto){
        if (cpp2::cmp_less(a,b)) {
            return a; 
        }
        else {
            return b; 
        }
    }

    The general rule of cppfront that I have learned is to think about my intentions rather than what I expect generated code to look like.

    In this example, if I want to read only from the variable, then it means I should use the in passing style. When you use inout passing style and don't write to the variable, cppfront will generate an error diagnostic to use in instead - check https://github.com/hsutter/708/blob/main/708.pdf (chapter 1.3.2 Inout parameters).

    Please note that in passing style is the default so that you can write the min function as:

    min: (a, b) -> forward _ = {
        if a < b {
            return a;
        }
        else {
            return b;
        }
    }
  4. jcanizales commented on Feb 7, 2023

    @jcanizales

    @leejy12 as you said:

    inout x: X means "present me an X I can read and write"

    const means "you can't write to it". So you cannot use inout with it. Simple as that.

    "The function must contain a non-const use of X" is just the compiler being helpful and ensuring you're not making a mistake. Because you want it to be const, it can't be inout. It must be in, as @filipsajdak shows.

  5. leejy12 commented on Feb 7, 2023

    @leejy12
    Author

    Ah, I forgot about generic _ parameters. Thanks for your answers @filipsajdak @jcanizales !

  6. hsutter commented on Mar 1, 2023

    @hsutter
    Owner

    Yes, thanks to @leejy12 for raising the question...

    and to @filipsajdak who wrote this...

    The general rule of cppfront that I have learned is to think about my intentions rather than what I expect generated code to look like.

    and to @jcanizales who wrote this...

    const means "you can't write to it". So you cannot use inout with it. Simple as that.

    ... which made me realize that const qualification doesn't make sense for parameters (except copy parameters, because only those are expressing the intent of really making a new variable... all the others are conceptually, and usually actually, aliases for the original argument).

    So I should just disallow const for non-copy parameters... done, commit coming up...

    Thanks again!

  7. JohelEGP commented on Apr 13, 2023

    @JohelEGP
    Contributor

    @leejy12, what you try to do is probably:

    min: (in a, in b) -> forward _ = {
        if a < b {
            return a;
        }
        else {
            return b;
        }
    }

    That will generate the following code:

    [[nodiscard]] auto min(auto const& a, auto const& b) -> decltype(auto){
        if (cpp2::cmp_less(a,b)) {
            return a; 
        }
        else {
            return b; 
        }
    }

    -- Extract from #250 (comment)

    That no longer works: https://godbolt.org/z/fh45f1ecT.

    main.cpp2(3,9): error: a 'forward' return type cannot return an 'in' or 'move' parameter
    main.cpp2(6,9): error: a 'forward' return type cannot return an 'in' or 'move' parameter
    
  8. JohelEGP commented on May 22, 2023

    @JohelEGP
    Contributor

    Here's why: #248 (comment).
    Feels like the usual entanglement of lifetime and something else (Was it value category? The shape of the parameter?).
    An attempt to provide safety that removes legit use-cases.

  9. JohelEGP commented on May 22, 2023

    @JohelEGP
    Contributor

    I feel like I remember now.
    It feels like when a type author has a function that takes an reference to const:

    struct t {
      void f(const std::string&);
    };

    And in an attempt to be useful, they overload it with:

    struct t {
      void f(const std::string&);
      void f(std::string&&) = delete; // No, unsafe! Use the other overload.
    };

    But that's conflating lifetime and value category.
    And it has unintended consequences or removes legit use-cases
    (e.g. t().f(""), where the caller knows its safe).

  10. JohelEGP commented on May 22, 2023

    @JohelEGP
    Contributor

    I could suggest to revert the prohibition of forward returning an in parameter.
    A templated min would work,
    but a concrete min could return a reference to a local (see thread from #343 (comment)).
    That relies on an implementation detail (the optimization done by for in parameters).

    I could suggest for an in parameter to always lower to a Cpp1 reference
    if the function's return type is by forward.
    That's special-casing.

    I could suggest to permit const-qualifying an inout parameter.
    But out and inout are meant to be written to.

    I really feel that the correct answer to support this use-case
    is something like a reference (see thread from #343 (comment)).

    I could suggest a new parameter direction,
    arg, that behaves like in/inout,
    but is meant to be returned or stored.
    So unlike in, object identity matters, so it's not optimized to pass by value.
    And const is not implied, and can be specified.
    But it overlaps a lot with in/inout.

  11. AbhinavK00 commented on May 23, 2023

    @AbhinavK00

    I could suggest to revert the prohibition of forward returning an in parameter.

    I argued for this some time back and still think it should be done.
    I think the best way to view in parameters is to see them as const& which are sometimes optimised to copies if certain conditions are met. I don't think a new convention is needed (6 are already too much).

  12. JohelEGP commented on May 23, 2023

    @JohelEGP
    Contributor

    I agree with that starting at #250 (comment).
    But not as a way to implement std::min because

    That relies on an implementation detail (the optimization done by for in parameters).
    -- #250 (comment)

  13. JohelEGP commented on Jun 9, 2023

    @JohelEGP
    Contributor

    This works, for now: https://cpp2.godbolt.org/z/rnKoEcaae.

    min: <T> (a: T, b: T) -> std::type_identity_t<decltype((a))> = {
        if a < b {
            return a;
        }
        else {
            return b;
        }
    }
    

    Not for concrete min.
    And someday even this example could be optimized like the concrete parameter (in x: int).

  14. JohelEGP commented on Sep 17, 2023

    @JohelEGP
    Contributor

    const means "you can't write to it". So you cannot use inout with it. Simple as that.

    ... which made me realize that const qualification doesn't make sense for parameters (except copy parameters, because only those are expressing the intent of really making a new variable... all the others are conceptually, and usually actually, aliases for the original argument).

    in parameters are optimized, and the difference is observable (#686).
    How about allowing inout _: const _ parameters to mean an in parameter with an address?
    That way, we can implement std::min (concrete or generic) in Cpp2.

  15. AbhinavK00 commented on Sep 17, 2023

    @AbhinavK00

    But don't you think that is is the kind of exceptions that cpp2 is trying to solve.

    A simple solution would be to make in parameters always as const& when forward parameters are returned.
    (But this MAY not cover all use cases, gotta think it out).

  16. JohelEGP commented on Sep 17, 2023

    @JohelEGP
    Contributor

    This made my think about parameter aliasing.

    One way to look at the suggestion of allowing inout _: const _ parameters
    is that out and inout allow modifying passed arguments, and thus alias.
    By adding const to inout, we remove the modification aspect, but retain aliasing.

    Another thought of mine is making all in parameters not alias if that was possible.
    And to require the inout _: const _ spelling for otherwise aliasing in parameters.
    Perhaps that way we can get a better default and more performance,
    like Rust parameters as I once understood them (I think it was after watching a talk by David Sankel).

  17. AbhinavK00 commented on Sep 17, 2023

    @AbhinavK00

    Rust parameters are faster because they're passed with restrict.
    The model that cpp2 uses is superior in many ways, (hylo uses kinda close model).

    1. If these kinda hacks are required, may as well fall back to references.
    2. How about using pointers to indicate aliasing so we get performance AND safety by default when using the passing conventions?
      I'm not sure I fully understand, but that was from what I understood.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions