cppfront icon indicating copy to clipboard operation
cppfront copied to clipboard

[SUGGESTION] Documenting `==` as `constexpr` and using unified declaration syntax for aliases

Open vladvrabie opened this issue 1 year ago • 8 comments

TLDR: Implementing function/object aliases as constexpr is confusing/don't work as aliases. == should mean constexpr. Aliases can use the unified declaration syntax name : kind = statement.

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? Not in C++, but in Cpp2 it should simplify the way it is tought and how the user thinks about / uses the syntax.

I've been learning Cpp2 since the documentation website came out and it very much feels like learning a new language. I think the perspective of a 'beginner'/'newcomer' is valuable and can teach us how Cpp2's syntax can be perceived and understood.

For a newcomer, it is difficult to undestand what == does and how it behaves (or why it is inconsistent) when it is tought as creating aliases. Some aliased elements (namespace/type) behave different than others (function/object) because of the chosen implementation (using vs constexpr).

Unified declaration syntax for namespace/type aliases

For me, an alias means "giving a new name to something". It feels more natural to use the unified syntax and I think the syntax for these aliases can be simplified to:

chr  : namespace = std::chrono;
imap : <T> type  = std::map<i32, T>;

(taken from the documentation website)

This is distinct from declaring namespaces/types (which requires {} after =).


Extra discussion: More or less using?

  • More: If the above syntax for aliases is not clear enough, we could make it stand out with something like using chr : namespace = std::chrono;
  • Less: We could eliminate the using statements in the syntax to bring names with something along the lines of _ : namespace = std::views; or * : namespace = std::views; or : namespace = std::views;; Credits to @farmerpiki for this

I would personally leave things as they are, no more or no less using.


Function aliases are not... aliases

If I have a function and I want to create an alias to it, as a (hypothetical) newcomer I would think to use ==. The overall aliases section in the documentation even says it lets me "define a synonym for" something.

  • For an alias, using the same function signature seems natural, but this gives the error 'return': cannot convert from 'cpp2::i32 (__cdecl *)(const int)' to 'cpp2::i32'

    square: (i: i32) -> i32 = i * i;
    square_alias: (i: i32) -> i32 == square;
    
  • To solve the error, try auto return type. This compiles and runs, but square_alias is not a true alias, it returns the address of square

    square: (i: i32) -> i32 = i * i;
    square_alias: (i: i32) -> _ == square;
    
  • Deduced type. It compiles and runs, square_alias is a true alias, but this is technically not a function alias anymore, but an object alias

    square: (i: i32) -> i32 = i * i;
    square_alias: _ == square;
    

These examples show that the "function alias syntax" does not fulfill the user's intent of defining an alias like in the namespace/type case.

I think the problem lies in the fact that these function aliases are implemented as new constexpr functions. And when we write code like square: (i: i32) -> _ == i * i;, we are more interested in the constexpr part, not on the alias part.

Once we free the constexpr functions from the shackles of alias and users are tought that == means constexpr, it actually makes the syntax baggage lighter and enables the user to realize that == can be used inside of classes to create constexpr constructors and methods (which is also not documented anywhere).

And there are already other concepts in Cpp2 to deal with function aliases, such as std::function or function pointers.


Object aliases are also not aliases*

*Except when they are function pointers

Let us consider the case of object aliases now, just as above.

  • For an alias, using the same type seems natural, but this gives the error expression did not evaluate to a constant

    val: i64 = 1024;
    val_alias: i64 == val;
    
  • This would run if val were initialized on the same line as its declaration. But it doesn't work because it is initialized after val_alias.

    val: const i64 = 1024;
    val_alias: i64 == val;
    
  • This prints 2 different addresses, which means val_alias is not really an alias, but a copy, which is not what was expected/desired.

    val: std::array<int, 3> == (1, 2, 3);
    val_alias: _ == val;
    
    main: () = {
      std::cout << val[0]& << "\n";
      std::cout << val_alias[0]& << "\n";
    }
    

Same point as above: Using == for aliases does not create aliases (new names for existing objects), but actually allocates new constexpr objects.

What is the most natural way to make object aliases in Cpp1? References! But Cpp2 doesn't have that (...yet?). Other alternatives would be to use pointers or std::reference_wrapper.

vladvrabie avatar Aug 07 '24 15:08 vladvrabie

This is two orthogonal issues in one, right? The fact that currently aliases aren't real aliases, and a proposed new syntax for type and namespace aliases.

jcanizales avatar Aug 07 '24 22:08 jcanizales

What started me thinking on this was Herb's Autumn update, section "Generalized aliases+constexpr with ==".

Because the two are put under the same syntax, I thought I should highlight the problem with (some) aliases and propose a solution (well, 2 solutions). I suggested the new syntax here because I wrote

For a newcomer, it is difficult to undestand what == does and how it behaves (or why it is inconsistent) when it is tought as creating aliases.

and I thought a proposal to separate the 2 features is beneficial and supports my argument.

Let me know if it's better to separate the issues. I can move the syntax proposal to a new issue.

vladvrabie avatar Aug 08 '24 06:08 vladvrabie

I also kinda think it should be 2 issues, also the constexpr fact about == maybe should be emphasised more on the documentation side... when @vladvrabie mentioned this initially I was seriously confused, also was confused when I saw the generated code for my "alias"...

farmerpiki avatar Aug 08 '24 07:08 farmerpiki

I think its fine to keep the issue all together like this for now.

DyXel avatar Aug 08 '24 07:08 DyXel

Yeah I was just trying to clarify what was being brought up, not commenting on whether it should be split in two separate threads.

jcanizales avatar Aug 08 '24 15:08 jcanizales

[Updated]

Thanks! I'm thinking about your examples, but I have a question...

I think you're saying you're surprised this doesn't work:

func: (i: i32) -> i32 = i * i;

func_alias: (i: i32) -> i32 == func;  // doesn't work

But I wouldn't expect it to work unless you include the argument list (note this doesn't work today):

func_alias: (i: i32) -> i32 == func(i);  // could be ok, requires argument list

To me this feels consistent with what we have today for type aliases:

map: <T,U> type = { /*... like std::map ...*/ };

map_alias: <T> type == map;  // doesn't work

map_alias: <T> type == map<i32, T>;  // ok, requires argument list

Does that make sense, that aliases still need argument lists?

I realize that doesn't fully address your question about identity. It does make me think that function aliases should additionally be lowered as always inline perhaps?

hsutter avatar Aug 10 '24 16:08 hsutter

Arguments are needed. Based your map example, it should be possible to do something like this:

func: (i: i32, j:i32) -> i32 = i * j;

func_alias: (j: i32) -> i32 == func(42, j);

I think this is similar to calling a function within lambda function in c++:

auto func_alias = [](int j) -> int {
  return func(42, j);
}

right?

filipsajdak avatar Aug 10 '24 18:08 filipsajdak

It's true that there are two related concepts here, "synonym" and "identity"... currently:

  • Type/function/object aliases are all synonyms (they always have the same meaning as the rhs, as if a use was replaced by the rhs).
  • A type alias is also an identity (it resolves to exactly the same type) which the other two do not (they don't resolve to the same function/object address).

The way to get function/object identity is to use a pointer (for non-templates). This is one reason I originally thought of making type aliases use pointer syntax, which would make it explicit that the code is talking about a pointer to a type. But I didn't explore that very far because then we'd have to distinguish a pointer to a type (a new compile-time concept) vs a pointer to an object of that type (ordinary pointer), which is very subtle, so I didn't pursue that.

  1. I do think the documentation should do better about explaining that == lowers to Cpp1 constexpr for functions and objects.
  2. Would the rest of the confusion be helped by using a word other than "alias" to denote that the declared thing is always a synonym for the rhs? Perhaps even rename "alias" to "synonym" to reduce the identity connotations?

I do think that so far it's been working well to use a single concept and syntax for type/function/object synonyms, names that are always the same as the rhs. But I could be wrong and I do appreciate the usability feedback from someone new to the syntax, and I'll keep thinking about it.

hsutter avatar Aug 10 '24 20:08 hsutter