Skip to content

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

Closed

Description

@leejy12

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.

Activity

  1. filipsajdak commented on Mar 11, 2023

    @filipsajdak
    Contributor

    I am currently missing that feature on the cpp2 side.

    I like the syntax.

  2. AbhinavK00 commented on Mar 11, 2023

    @AbhinavK00

    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.

  3. leejy12 commented on Mar 12, 2023

    @leejy12
    Author

    @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.

  4. msadeqhe commented on Mar 12, 2023

    @msadeqhe

    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.

  5. hsutter commented on Mar 12, 2023

    @hsutter
    Owner

    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.

  6. jcanizales commented on Mar 15, 2023

    @jcanizales

    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?

  7. AbhinavK00 commented on Mar 15, 2023

    @AbhinavK00

    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.

  8. JohelEGP commented on Mar 15, 2023

    @JohelEGP
    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.

  9. jcanizales commented on Mar 20, 2023

    @jcanizales

    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?

  10. hsutter commented on Apr 2, 2023

    @hsutter
    Owner

    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...

  11. AbhinavK00 commented on Apr 2, 2023

    @AbhinavK00

    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

  12. msadeqhe commented on Apr 2, 2023

    @msadeqhe

    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.

  13. jcanizales commented on Apr 3, 2023

    @jcanizales

    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.

  14. 17 remaining items

  15. AbhinavK00 commented on May 8, 2023

    @AbhinavK00

    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.

  16. msadeqhe commented on May 8, 2023

    @msadeqhe

    @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.

  17. AbhinavK00 commented on May 8, 2023

    @AbhinavK00

    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

  18. JohelEGP commented on May 8, 2023

    @JohelEGP
    Contributor

    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.

  19. leejy12 commented on May 8, 2023

    @leejy12
    Author

    @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.

  20. JohelEGP commented on May 8, 2023

    @JohelEGP
    Contributor

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

  21. msadeqhe commented on May 8, 2023

    @msadeqhe

    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.)

  22. msadeqhe commented on May 8, 2023

    @msadeqhe

    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.

  23. JohelEGP commented on May 8, 2023

    @JohelEGP
    Contributor

    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.

  24. msadeqhe commented on May 8, 2023

    @msadeqhe

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

  25. JohelEGP commented on May 8, 2023

    @JohelEGP
    Contributor

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

  26. msadeqhe commented on May 8, 2023

    @msadeqhe

    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);
    }
  27. JohelEGP commented on May 8, 2023

    @JohelEGP
    Contributor

    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.
  28. added a commit that references this issue on Dec 5, 2023
    c2da23c
  29. added a commit that references this issue on Feb 4, 2024
    63efa6e
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