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

[SUGGESTION] Add support for defining type aliases. #273

Closed
leejy12 opened this issue Mar 11, 2023 · 43 comments
Closed

[SUGGESTION] Add support for defining type aliases. #273

leejy12 opened this issue Mar 11, 2023 · 43 comments

Comments

@leejy12
Copy link

leejy12 commented Mar 11, 2023

Recently cppfront added the ability to define user-defined types. I want to extend this feature to be able to declare type aliases. My proposed syntax looks like this:

DWORD: type = u32;

// templated type aliases
MyPair: <T> type = std::pair<T, T>;

// nested aliases work too
MyVector: <T> type = {
    size_type: type = std::size_t;
}

This will be compiled to (skipping boilerplate):

using DWORD = cpp2::u32;

template<typename T> using MyPair = std::pair<T,T>;

template  <typename T> class MyVector {
    public: using size_type = std::size_t;
};

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, this syntax uses the l-to-r declaration syntax already used widely in Cpp2, so there's no need to teach the syntax
using alias_name = type_name.

I have already implemented this feature locally so I can provide a PR.

@filipsajdak
Copy link
Contributor

I am currently missing that feature on the cpp2 side.

I like the syntax.

@AbhinavK00
Copy link

AbhinavK00 commented Mar 11, 2023

Nice feature, but one thing I'd like to point out, it's important to have distinction between actual user defined types and type aliases. The syntax could be same but we can have a different keyword instead of type.
One example could be typealias, so the given examples would be

DWORD: typealias = u32;

// templated type aliases
MyPair: <T> typealias = std::pair<T, T>;

// nested aliases work too
MyVector: <T> typealias = {
    size_type: typealias = std::size_t;
}

Any keyword that fits can be chosen but it's the distinction which I emphasize that should be present.

@leejy12
Copy link
Author

leejy12 commented Mar 12, 2023

@AbhinavK00 You make a very good point and I've actually thought about this while implementing.

Currently in my implementation, if initializer.statement is a statement_node::compound, cppfront prints a new class. If it's a statement_node::expression (no braces) then cppfront prints using ..., but I felt this approach was very hacky.

If the distinction between UDTs and type aliases is significant enough to warrant an introduction of a new keyword, I could try implementing it that way as well.

@msadeqhe
Copy link

msadeqhe commented Mar 12, 2023

Nice feature, but one thing I'd like to point out, it's important to have distinction between actual user defined types and type aliases. The syntax could be same but we can have a different keyword instead of type. One example could be typealias, so the given examples would be

DWORD: typealias = u32;

// templated type aliases
MyPair: <T> typealias = std::pair<T, T>;

// nested aliases work too
MyVector: <T> typealias = {
    size_type: typealias = std::size_t;
}

Any keyword that fits can be chosen but it's the distinction which I emphasize that should be present.

I've read in a comment from @hsutter that he wants to use is keyword for class inheritance, maybe is can be a good candidate for type aliases too, because a type alias is exactly the same as the other type without any additional definition. I don't know the syntax of class inheritance yet, but maybe it would be something like this:

DWORD: type is u32;

Edit: Also is keyword doesn't make a good distinction between user defined types and type aliases. I withdraw it! In this case, = seems more natural than is keyword.

@hsutter
Copy link
Owner

hsutter commented Mar 12, 2023

Good suggestion. I do intend to implement aliases down the road, and I haven't settled on a syntax for them yet.

One issue with DWORD: type = u32; is that, while it's not legal today and therefore is available, it could close a door to future development because it's a syntax we might naturally use for strong typedefs (the syntax implies "declare a new separate thing initialized from this existing thing," like with x: int = 42; where x is not an alias for 42, it's a copy).

I am considering using is declaratively in a couple of places, such as to declare base classes (coming soon). Here's the key test I'm using to decide whether to use is declaratively: If you declare something with is, then it should be guaranteed to later evaluate true if you ask the equivalent is test, and that is test should be supported. In this case, if DWORD is an alias for u32, then if we make DWORD is u32 be true (not currently supported, but reasonable), then a syntax that includes is would be a candidate for declaring the alias.

@jcanizales
Copy link

Nice feature, but one thing I'd like to point out, it's important to have distinction between actual user defined types and type aliases. The syntax could be same but we can have a different keyword instead of type.

What's your motivation for this, @AbhinavK00?

@AbhinavK00
Copy link

No real motivation I guess but there should be a distinction between strong typedefs and just aliases to typenames.
Consider the two syntaxes considered above,

DWORD : typealias = i32;

This is just another name and DWORD can be used anywhere in place of u32, this doesn't have many use cases.
Then there's this,

DWORD : type is i32;

This is like wrapping an i32 in a struct, it's different from i32, can be casted to an i32 and need to be casted explicitly if using it in place of an i32 and vice-versa.

So no real motivation, but I just put it out in case if others agree.

@JohelEGP
Copy link
Contributor

One issue with DWORD: type = u32; is that, while it's not legal today and therefore is available, it could close a door to future development because it's a syntax we might naturally use for strong typedefs (the syntax implies "declare a new separate thing initialized from this existing thing," like with x: int = 42; where x is not an alias for 42, it's a copy).

But what is a "strong typedef"? It'd be great if, down the road of development of Cpp2, we could find the answer meant for Cpp2. Although, with metaclasses, we might be able to author better answers than today's Cpp1.

@jcanizales
Copy link

jcanizales commented Mar 20, 2023

As a library author, when you export a type name sometimes it refers to a class you defined, and sometimes you want it to be just an alias to something else, without leaking that detail to the user of the library.

In current C++ the leakage is unavoidable, for example because the user can forward-declare your classes, but not an alias. Hopefully in Cpp2 this specific case is not an issue because forward-declaring is made unnecessary. But the general request remains, of letting a library author export an alias that is indistinguishable from a type's "true" name.

But what is a "strong typedef"?

I'm guessing Herb is thinking of compile-time modification of types, for which whether you're modifying the original or a copy makes a difference?

@hsutter
Copy link
Owner

hsutter commented Apr 2, 2023

On thinking about this further, two things:

  1. I want to handle aliases in general, not just type aliases -- also namespace, function, and object aliases. For example, namespace aliases already exist in Cpp1 and are useful. I don't want to lose that convenience, I just want to make it more regular with the rest of the language.

  2. I want a consistent general syntax that works for all of these. So for example the word typealias is tempting, but that word would work well only for types, and then we'd be getting pressure to add namespacealias etc. too.

  3. That syntax should be a declaration syntax, since we are declaring a new name -- it just happens to be an alias rather than a standalone entity. So the syntax should start with name :, but should somehow be different from the name : type = value Cpp2 uses for all declarations to express that this is an alias, not a standalone entity.

So after some thought of brainstorming options, include ones that include is, the one I like the best is to use exactly the declaration syntax, but with == to connote equality with an existing entity, instead of = which connotes setting the new entity's value. For example:

lit: namespace == ::std::literals;

pmr_vec: <T> type
    == std::vector<T, std::pmr::polymorphic_allocator<T>>;

func :== some_original::inconvenient::function_name;

vec :== my_vector;  // note: const&, aliases are never mutable

Checking it in now...

@hsutter hsutter closed this as completed in 63efa6e Apr 2, 2023
@AbhinavK00
Copy link

AbhinavK00 commented Apr 2, 2023

Question: What does the following line mean?

smth :== something::smth;   

And the point about const& too, didn't get those.

Also, these are same as using counterparts to cpp, yes?

Edit: Nevermind, got it

@msadeqhe
Copy link

msadeqhe commented Apr 2, 2023

Thanks. Nice reasonable syntax.

I think x :== y; is a short form for both x: function == y; and x: variable == y;, because function and variable aliases are distinguishable from y.

Maybe also z in x: z == y; could be optional for both namespaces and types.

@jcanizales
Copy link

And the point about const& too, didn't get those.
Edit: Nevermind, got it

I didn't 🙂 Is it supposed to mean that you can't "assign" something to the alias to bind it to something else? (But you already can't assign anything to a type name). const vector& means the vector can't be written to, so I hope it's not that.

@AbhinavK00
Copy link

I think it is like, you can't change the meaning of an alias. Something like the following:

vec :== my_vector; //created alias

//lots of code here

vec == std::vector;  //illegal

I think it is that, but someone definitely chime in if i'm wrong or there's a better explanation.

@gregmarr
Copy link
Contributor

gregmarr commented Apr 3, 2023

const vector& means the vector can't be written to, so I hope it's not that.

It is that:

Aliases are always a non-mutable view of the original entity. This is natural for namespace and type and function aliases, since those kinds of entites can't be mutated, but it bears calling out for an object alias which behaves as auto const&.

@AbhinavK00
Copy link

Wait, I thought cpp2 was to remove references altogether? Isn't that just references in other form.

@JohelEGP
Copy link
Contributor

JohelEGP commented Apr 3, 2023

I think that's an implementation detail. The point is having a general "alias" syntax.

@AbhinavK00
Copy link

It's still redundant, one could do the same thing with non-mutating pointers (with a bit more writing). It's like, making different things look same.

@JohelEGP
Copy link
Contributor

JohelEGP commented Apr 3, 2023

Then variable aliases would be different than the others, as it would require punctuation to refer to the aliased entity. 63efa6e's commit message is very informative.

@AbhinavK00
Copy link

My point is, object aliases are not needed. You can already do that with pointers, which existed before the commit and aliasing objects is different from aliasing classes and namespaces, the former may/may not have a level of indirection with it whjle the latter does not (I have no idea how using is implemented but I'd assume so).

@jcanizales
Copy link

jcanizales commented Apr 3, 2023

A fundamental difference between aliases to namespaces/types/functions, and C++ references to objects, is that the former things are "static" as in their lifetime is the lifetime of the program, and they're immutable. Their aliases are resolved by the compiler. C++ references to objects, instead, are pointers and are resolved and runtime (so come with a set of issues with lifetime and concurrency).

From the way I'm seeing this implemented syntax, you cannot declare the type of a variable to be "alias to an object" (which would be equivalent to a C++ reference). So these aliases are just a "second name" that's resolved only at compile-time, and can't "escape" the current scope like a pointer can.

@jcanizales
Copy link

In other words, this seems to me to be "give a second new name to this thing during compilation", and not "take the memory address of this and pass it around at runtime".

@AbhinavK00
Copy link

The whole point of aliases is to give a better name to things that you don't have the control to rename like namespaces and classes/functions in them. Objects are made by the user, he can name them whatever he wants, why would he refer to it with a different name. This feature is so unnecessary.

@jcanizales
Copy link

Yeah I wasn't commenting on the usefulness. Although "object aliases" is how function aliases are currently implemented, per the commit Johel pointed to. So I figure the use cases for aliasing a non-function object would be the same as why you'd want to alias a function:

  • When you didn't create it yourself, but it came from a library you're consuming.(Namespaces not only contain types and functions, they often also contain constant objects).

  • When you need to expose some particular name to consumers of your code. For functions and types this is often done in traits structs for template metaprogramming.

Anyway I feel that "make aliases work for non-function objects too" wasn't done to satisfy existing use cases people were clamoring for. Rather to not add an exception to the language. Given the intended unification of "namespaces are like classes", "functions are just objects of function type", and all those things being declared with the same syntax.

@filipsajdak
Copy link
Contributor

References are for parameter passing, including range-for. Sometimes they’re useful as local variables, but pointers or structured bindings are usually better. Any other use of references typically leads to endless design debates.

See: https://herbsutter.com/2020/02/23/references-simply/

@AbhinavK00
Copy link

That supports my point!

@JohelEGP
Copy link
Contributor

JohelEGP commented Apr 4, 2023

I suppose you'd want to undo this being a reference in Cpp2.

@AbhinavK00
Copy link

AbhinavK00 commented Apr 4, 2023

It's supposed to be an lvalue.
Edit: I'll stop, sorry for the argument

@JohelEGP
Copy link
Contributor

JohelEGP commented May 7, 2023

This is now #438.

Moved contents.

So after some thought of brainstorming options, include ones that include is, the one I like the best is to use exactly the declaration syntax, but with == to connote equality with an existing entity, instead of = which connotes setting the new entity's value.

A wild thought.

Maybe a namespace declaration should use += instead of =.
So ns: namespace = { would become ns: namespace += {.

+= denotes in-place addition, not definition.
But notice how : remains despite +=.
So it both (re)defines the namespace's identifier, which is valid, and adds to it.
Then all remaining uses of : /*signature*/ = would denote definition of the non re-definable kind, which is a bigger plus.

How does that fit with the current Cpp2?
What about with your idea to replace namespace with a metafunction, @hsutter?

@JohelEGP
Copy link
Contributor

JohelEGP commented May 8, 2023

By supporting nested namespace definition, the distinction from other declarations raises.

x::y: type = { } // Error!
a::b: namespace += { } // Perfectly OK.

@AbhinavK00
Copy link

Then all remaining uses of : /signature/ = would denote definition of the non re-definable kind, which is a bigger plus.

Varibles can be re-assigned so I don't think : /*signature*/ = denotes something of non-redefinable kind and you can also add functions to classes.

@msadeqhe
Copy link

msadeqhe commented May 8, 2023

@JohelEGP, I like your idea.

@AbhinavK00, Because = in identifier: /*something*/ = /*definition*/ either defines or redefines identifier. That's why we cannot add members to types after they are defined.

On the other hand, += is similar to extension methods in C#. It's like if this was possible in Cpp2:

point: type = {
    x: int = 0;
    y: int = 0;

    operator=: (out this, arg_x: int, arg_y: int) = {
        x = arg_x;
        y = arg_y;
    }
}

point: type += {
    // Extension method
    print: (this) = {
        std::cout << x << ", " << y << '\n';
    }
}

Namesapces inherently won't be redefined, so only new declarations are added to them. It's going to be like this example in a similar manner to the previous example:

// Maybe they should be defined at first with `=`
x: namespace = {}

x: namespace += {
    func: () = {}
}

x: namespace += {
    another_func: () = {}
}

main: () = {
    x::func();
    x::another_func();
}

Ignore the following

Unhide contents...

If Cpp2 could support extension methods, namespaces could be another metaclass function:

// Maybe they should be defined at first with `=`
x: @namespace type = {}

x: @namespace type += {
    func: () = {}
}

It would merge namespace classes (types which only have static members) and namespaces to a single concept.

EDIT: Types and namespaces have logically different behaviours in many ways in Cpp2. So you can ignore this part.

@AbhinavK00
Copy link

Because = in identifier: /something/ = /definition/ either defines or redefines identifier

Then all remaining uses of : /signature/ = would denote definition of the non re-definable kind, which is a bigger plus.

if = is able to redefine, then anything declared by : /*something*/ = /*definition*/ should be redefinable but thats not true for functions. I like @JohelEGP 's definition better but variables are an exception to that, not sure about classes

@JohelEGP
Copy link
Contributor

JohelEGP commented May 8, 2023

Then all remaining uses of : /signature/ = would denote definition of the non re-definable kind, which is a bigger plus.

Varibles can be re-assigned so I don't think : /*signature*/ = denotes something of non-redefinable kind and you can also add functions to classes.

Variables can be assigned to, yes.
But only once, the first time, may it have : indicating definition.
Other assignments are not redefinitions.

Shadowing does redefine what the identifier means for the current scope.
And is distinguished from : namespace +=, which really does append, and doesn't shadow.

// Maybe they should be defined at first with `=`
x: namespace = {}

I disagree.
Maybe that should be the case for the extension methods you describe.
But : is definition, and namespaces always append, so requiring = first is just needless friction.

@leejy12
Copy link
Author

leejy12 commented May 8, 2023

@JohelEGP I like your idea.

I think it would be better if this discussion about namespaces (and namespace metaclass) were moved to a new, open issue for better visibility.

@JohelEGP
Copy link
Contributor

JohelEGP commented May 8, 2023

The entry bar is a bit higher for suggestions, compared to continue piling up on an issue's discussion. But I did it: #438.

@msadeqhe
Copy link

msadeqhe commented May 8, 2023

if = is able to redefine, then anything declared by : /*something*/ = /*definition*/ should be redefinable but thats not true for functions.

I mean that = can define or redefine identifiers in new scopes.

Currently Cpp2 doesn't support local functions, but this is an example:

a: = 0;
x: () = {};
{
    a: = 1;
    // It seems X is redefined, but underhood it's another function.
    x: () = {};
}

(This example is just my thought about how local functions may be available in the future.)

@msadeqhe
Copy link

msadeqhe commented May 8, 2023

I disagree.
Maybe that should be the case for the extension methods you describe.
But : is definition, and namespaces always append, so requiring = first is just needless friction.

More precisely, : is for declaration, and the first = is for definition:

point: type = {
    operator=: (out this, x: int, y: int) = {}
}

main: () = {
    // It declares `x` is a `point`.
    x: point;

    // It defines `x` with constructor.
    // It generates: `x.construct(0, 0);`
    x = (0, 0);
}

You're right. Requiring to define namespaces with = for the first time, can lead to complicated code.

@JohelEGP
Copy link
Contributor

JohelEGP commented May 8, 2023

More precisely, : is for declaration, and the first = is for definition:

I wonder if that's how you'd describe it in Cpp2.
The seemingly-Cpp2-declaration is lowered to Cpp1 as a definition.
Without documentation, I can only guess as to the terminology.

@msadeqhe
Copy link

msadeqhe commented May 8, 2023

A declaration without = cannot be used, because it's not completed yet for use in Cpp2, either it's uninitialized or undefined or ...

@JohelEGP
Copy link
Contributor

JohelEGP commented May 8, 2023

The thing is, = without : is assignment.
If the declaration is followed by an assignment, where's the definition?

@msadeqhe
Copy link

msadeqhe commented May 8, 2023

The first assignment is the definition:

point: type = {
    operator=: (out this, x: int, y: int) = {}
}

main: () = {
    // Declaration and definition
    v: point = (0, 0);

    // Declaration
    u: point;

    // Definition
    // It generates: `u.construct(0, 0);`
    u = (0, 0);

    // ERROR! This is not a definition.
    // BTW it generates wrong Cpp1: `u.value() = 1, 1;`
    u = (1, 1);
}

@JohelEGP
Copy link
Contributor

JohelEGP commented May 8, 2023

It seems plausible that the definite first assignment is the definition.
But I'm not convinced.

Thinking about definite first assignment made me remember that Cpp2 doesn't have forward declarations.
So perhaps it's not a matter of declaration vs. definition.

In Cpp2, things that can be without an initializer are:

  • Variable declarations, which require a definite first assignment.
  • virtual this functions, which are pure virtual.
  • operator==, operator<=>, which are defaulted.
  • Parameters of templates, functions, and the one of a for loop's parameterized-statement.

zaucy pushed a commit to zaucy/cppfront that referenced this issue Dec 5, 2023
An alias can be to a namespace, type, function, or object.

Aliases are always a non-mutable view of the original entity. This is natural for namespace and type and function aliases, since those kinds of entited can't be mutated, but it bears calling out for an object alias which behaves as `auto const&`.

The current syntax is like non-alias declarations, but with `==` to connote equality with an existing entity, instead of `=` which connotes setting the new entity's value. For example:

    lit: namespace == ::std::literals;

    pmr_vec: <T> type
        == std::vector<T, std::pmr::polymorphic_allocator<T>>;

    func :== some_original::inconvenient::function_name;

    vec :== my_vector;  // note: const&, aliases are never mutable

Note: Function aliases are subsumed under the object case, so you can't declare a function alias with an explicit signature. Rationale:
    - if there's no need for a (repetitive) explicit signature,
        the object case covers it
    - if the parameters/returns must be exact (no conversions),
        a pointer to function covers it
    - if parameter/return conversions are allowed,
        std::function covers it
But if we do learn about a reason to add support for function aliases having an explicit signature, and possibly with conversions to/from the parameter/return types, they'll be a natural fit here.
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

8 participants