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

Proposal: Add support for typedef (like in C++) #1300

Open
aaronfranke opened this Issue Feb 3, 2018 · 44 comments

Comments

Projects
None yet
@aaronfranke

aaronfranke commented Feb 3, 2018

Currently, in C#, there is no typedef like in C++. typedef is useful, for example, when creating a game engine with simultaneous support for float and double (an abstract type, via typedef, would be used in place of either type). Or when creating a descriptive short-hand for types without objects.

Would it be possible to add this functionality to the C# language spec?

@benaadams

This comment has been minimized.

benaadams commented Feb 3, 2018

You can do aliasing currently for types based on defines with use using ... = ... with #if to achieve the float/double

#if REAL_IS_DOUBLE
using real = System.Double;
#else
using real = System.Single;
#endif
real Add(real a, real b)
{
    return a + b;
}

Or a native int type (assuming you set #define BIT64 for a 64 bit build)

#if BIT64
using nint = System.Int64;
#else
using nint = System.Int32;
#endif
@stakx

This comment has been minimized.

Contributor

stakx commented Feb 3, 2018

... and you have to repeat the alias definition in each source file where you want to use the alias.

@aaronfranke

This comment has been minimized.

aaronfranke commented Feb 3, 2018

Indeed. Example of an abstract float type in C++: Godot vector3.h There is no need to write a 5-line definition at the top of each file (in fact, not even 1 line).

@MkazemAkhgary

This comment has been minimized.

MkazemAkhgary commented Feb 3, 2018

... and you have to repeat the alias definition in each source file where you want to use the alias.

I don't think it would be that hard for compiler to treat it as public, yet its not supported.

@MkazemAkhgary

This comment has been minimized.

MkazemAkhgary commented Feb 3, 2018

Another alternative, doesn't work everywhere but it could be enough for your case since you are working with simple primitive types.

I don't think there is any performance overhead to this approach. this struct could be optimized away by compiler and become exactly its containing type. likely I could be wrong.

using ValueType = System.Double; // you only need to change here. maybe use #if directive

public struct RealNumber
{
    ValueType value;

    public RealNumber(ValueType value)
    {
        // you can also perform some data validation here.
        this.value = value;
    }

    public static implicit operator ValueType(RealNumber value) => value.value;
    public static implicit operator RealNumber(ValueType value) => new RealNumber(value);
}
@bondsbw

This comment has been minimized.

bondsbw commented Feb 3, 2018

@MkazemAkhgary using is scoped to the current file, whether it be for importing namespaces, class aliases, or importing static methods.

I would like a global class alias but it would probably be better to use a different keyword.

@aaronfranke

This comment has been minimized.

aaronfranke commented Feb 3, 2018

it would probably be better to use a different keyword.

How about typedef? This would be most familiar to people coming from C++.

@aaronfranke aaronfranke changed the title from [Feature Request] Add support for typedef (like in C++) to Proposal: Add support for typedef (like in C++) Feb 4, 2018

@aluanhaddad

This comment has been minimized.

aluanhaddad commented Feb 5, 2018

@aaronfranke typedef in C++ has been superseded by the enhanced using alias introduced in C++11 so the familiarity argument doesn't track. If anything using should be enhanced in C#. I also don't see how it is an abstract type. It is just an alias that you can optionally use.

@aaronfranke

This comment has been minimized.

aaronfranke commented Feb 5, 2018

It is useful as an abstract type because you can have situations where a data type can be either A or B, but in-file you just refer to it as type X. A single line can then be changed to make it become A or B.

@aluanhaddad

This comment has been minimized.

aluanhaddad commented Feb 5, 2018

@aaronfranke

It is useful as an abstract type because you can have situations where a data type can be either A or B, but in-file you just refer to it as type X. A single line can then be changed to make it become A or B.

I don't think this is a great way to factor code. I have no doubt it works well enough, but it is not expressive. A new language feature should enable clearer expression of ideas.

@contextfree

This comment has been minimized.

contextfree commented Feb 8, 2018

It seems like it should be possible at least to have assembly-scoped using. "internal using"?

For me the use case would be more about wanting to avoid the syntactic overhead of putting IBlahBlahBlah everywhere, but not wanting to require clients to use a specific wrapper type.

@aluanhaddad

This comment has been minimized.

aluanhaddad commented Feb 16, 2018

@contextfree that's an excellent choice of syntax conveying the semantics perfectly. There's also a proposal, #1180, for namespace level method declarations. Such a feature would I think cover that proposal, which I quite like, as well:

internal using static Namespace.Utilities;

namespace Namespace
{
    static class Utilities
    {
        public static void WriteLine(object value) => Console.WriteLine(value);
    }
}

So while I dislike the example use case of this proposal, your suggestion would make it generalizable to much more interesting scenarios.

@Danielku15

This comment has been minimized.

Danielku15 commented Apr 19, 2018

To extend this proposal with a more modern approach than typedefs:

Haxe, a language for cross-compilation to other languages, has a concept called [abstract types] (https://haxe.org/manual/types-abstract.html) beside the typedefs .

It allows you to define a type which is evaluated to its underlying type during compile time. For this type you can define custom operators, methods. If an abstract type cannot have additional members as the storage will be the defined underlying type, but it can have methods, properties and operators. Haxe also supports inlining of methods which might any overhead. If you do not inline the methods, there will be a class holding the methods as statics. This way you could even implement a "int" that can only hold even numbers.

I personally do not like the fact that they call them "abstract types" as abstract means something else in C# and Java. But by using typedef as keyword we could build a even more powerful system than recycling maybe the C++ style typedef approach.

typedef Color : int 
{
    public inline Color(int i) 
    {
        this = i;
    }
    
    public inline Color(Color c)
    {
        this = (int)c;
    }
    
    public inline byte Alpha => (this >> 24) & 0xFF;
    public inline byte Red => (this >> 16) & 0xFF;
    public inline byte Green => (this >> 8) & 0xFF;
    public inline byte Blue => this & 0xFF;
    
    public static inline Color FromArgb(byte a, byte r, byte g, byte b)
    {
        return new Color((a << 24) | (r << 16) | (g << 8) | b);
    }
    
    public static inline implicit operator int(Color c) 
    {
        return c;
    }
    
    public static inline implicit operator Color(int i) 
    {
        return new Color(i);
    }
}

typedef GameNumber 
#if BIT64
        : double
#else 
        : float
#endif
{
    public void DoSomething(GameEngine engine)
    {
        engine.PutToPool(this); // GameEngine.PutToPool(float f) or GameEngine.PutToPool(double d)
    }
    public static inline explicit operator long(Real c); // auto-operator via underlying type
    public static inline implicit operator Real(long i); // auto-operator via underlying type
}

There would be definitly some details to clarify like:

  • Auto exposing of underlying type methods
  • Inlining strategy (not sure if the C# compiler already can do that well)
  • Exposing of typedefs outside assemblies
  • Refection/Dynamic support

This would also allow a bit more strict typing within your library.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Apr 19, 2018

I'm not sure what that buys you on top of what you can already do today (just wrapping your value in a struct that has whatever API you want to expose).

@Danielku15

This comment has been minimized.

Danielku15 commented Apr 19, 2018

The benefit in this case is that on compile time the type is reduced to the underlying type. This means it can also be a class or interface that you wrap in this case. When you wrap it into a struct you will have a lot of wrapping/unwrapping ongoing. Plus: You will have a new type in your assembly and public interface even though you just wanted to have a wrapper with some more meaning. My proposal is more an extension of a typedef too maybe allow more logic. Also a simple typedef could be rebuild with a struct/class + implicit cast operators but in the end that's not the goal.

I saw especially in cross-platform compilation cases such types can be really useful especially if you have platform specific implementations.

The bullet point with "exposing outside assemblies" is maybe misleading. By this I did not mean directly that the typedef is exposed and embedded in the assembly, but rather if you have typedefs in your contract, you might need to consider that the conversion operator of the passed value is invoked at the beginning of the method. If you have a EvenInt type that ensures only even numbers, and somebody passes an odd integer to your method it might need to call your conversion function. Also the bullet point on reflection and dynamic might just end in the decision that typedefs will provide the functionality of the underlying type and additional members are lost.

@jnm2

This comment has been minimized.

Contributor

jnm2 commented Apr 19, 2018

This means it can also be a class or interface that you wrap in this case

I missed your point the first few times I read this line. Meaning, you want to be able to pass your typedef type to things expecting ISomeInterface or SomeBaseClass?

@Danielku15

This comment has been minimized.

Danielku15 commented Apr 20, 2018

Meaning, you want to be able to pass your typedef type to things expecting ISomeInterface or SomeBaseClass

Yes, you can basically wrap anything in the typedef, a class, interface, enum or struct (by this also standard data types like string). Just like in C++ the typedef becomes an alias for the original type.

The simple version of a typedef could be typedef MyFloat : float; In this case it is a simple alias. Variables of MyFloat can get float values assigned or be used anywhere where a float is needed. Also it provides all methods/properties etc. forwarded to the original type. A classical typedef or name-alias.

The extended version brings a bit more strict typing into the game. At compile time the alias also gets replaced with the underlying type. But additionally you can define specific operators, methods and maybe properties. The code of those might either be inlined for performance reasons or be placed in a static class with extension methods. It covers the use case that the typedef is more than a simple alias for convenience but really a entity in your assembly it has some more semantics and should follow some rules.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Apr 20, 2018

I'm not sure what that buys you on top of what you can already do today (just wrapping your value in a struct that has whatever API you want to expose). :)

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Apr 20, 2018

The benefit in this case is that on compile time the type is reduced to the underlying type. This means it can also be a class or interface that you wrap in this case.

Sure, that could also be the case when you wrap with a struct.

When you wrap it into a struct you will have a lot of wrapping/unwrapping ongoing.

Wait, why? The runtime is very smart about this. A struct that wraps a single value has no overhead. And because there is no inheritance/virtual/interfaces going on, practically all operations punch-through to the underlying value.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Apr 20, 2018

Plus: You will have a new type in your assembly and public interface even though you just wanted to have a wrapper with some more meaning.

That sounds like a virtue. You're defining new operations and whatnot. how is exposing that as a "typedef" better than just exposing as a type? Why do you need a new concept for this when this is a primary use case for one of hte existing concepts the system already provides?

@jnm2

This comment has been minimized.

Contributor

jnm2 commented Apr 20, 2018

@Danielku15

Meaning, you want to be able to pass your typedef type to things expecting ISomeInterface or SomeBaseClass

Yes, you can basically wrap anything in the typedef, a class, interface, enum or struct (by this also standard data types like string). Just like in C++ the typedef becomes an alias for the original type.

You can match this behavior today with a wrapper type using implicit conversions, and for interfaces, delegating implementations. The only thing this doesn't get you is reference equality with the wrapped value, if, say, you pass the wrapper to an object argument.

@Danielku15

This comment has been minimized.

Danielku15 commented Apr 22, 2018

That sounds like a virtue. You're defining new operations and whatnot. how is exposing that as a "typedef" better than just exposing as a type?

The thing is rather that you are not exposing the typedef. It stays assembly-internal during compile time only the methods might remain as extension methods to the underlying type.

Why do you need a new concept for this when this is a primary use case for one of hte existing concepts the system already provides?

I wasn't aware that the runtime is supposed to be that intelligent when it comes to simple wrapper structs. My worries in this case are that everywhere, where you pass on your struct, it is cloned even though it might not be needed. I work since years on various cross platform projects and used Haxe quite a lot. I always loved how you can write platform specific wrappers without causing any overhead on your library footprint. When I moved to C# cross platform compilation I was always missing such a feature a bit. Especially when cross compiling to C# to JavaScript a small footprint and inlining are an important factor.

But if the .net Runtime is very performant on this it might not be needed from this perspective.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Apr 22, 2018

I wasn't aware that the runtime is supposed to be that intelligent when it comes to simple wrapper structs. My worries in this case are that everywhere, where you pass on your struct, it is cloned even though it might not be needed.

This is only a concern if you have a large struct. i.e. a struct with many members in it. In this case, this is about a struct always wrapping exactly one value. In that case, the size of hte struct is exactly the same as the size of the underlying value, so the cost stays the same. :)

@chrisrollins

This comment has been minimized.

chrisrollins commented Jun 22, 2018

I continue to run into situations where I want this feature.

For example I have some methods that take string arguments, and then I thought of a reason to overload these methods a bunch of times because I wish to provide a lot of flexibility in my API. At some point I noticed that I had put the parameters in the wrong order for a few of the overloads. This is something I had to find out by either looking at the code carefully or by running into bugs during testing because the parameters share the same type.

My solution to avoid having this happen again is to define some structs that represent parameter types that the public overloads will actually pass to the implementation so that if I put them in the wrong order it will cause a compilation error.

I believe that there is runtime overhead to my approach and that bothers me. With typedef there would definitely be no runtime overhead in this situation, but I would still be able to use it to gain some type safety in the same way simply by using it to alias types like string as my parameter types instead of boxing them.

Another advantage of typedef is that it's more lightweight. In my case I had to define structs and give them public constructors even though all they really do is box a string value.

@louthy

This comment has been minimized.

louthy commented Jun 22, 2018

@theunrepentantgeek

This comment has been minimized.

theunrepentantgeek commented Jun 22, 2018

@chrisrollins

My solution ... is to define some structs that represent parameter types that the public overloads will actually pass to the implementation so that if I put them in the wrong order it will cause a compilation error.

I do this all the time - and I find that these semantic types (usually value types implemented with struct instead of class) tend to become pervasive through the codebase, representing key concepts.

I believe that there is runtime overhead to my approach and that bothers me.

My experience is that unless you're talking about a method that's called thousands or millions of times in a tight loop, the overhead won't be significant nor perceptible. If you have concerns, it's worth running a proper benchmark to prove or refute things. I did one this week using BenchMarkDotNet and discovered that the difference in my particular situation (which wasn't struct vs string) was a handful of nanoseconds.

@chrisrollins

This comment has been minimized.

chrisrollins commented Jun 23, 2018

@theunrepentantgeek you are right. It won't matter unless the code is used at scale. But that's the only time it ever matters for performance. To me it's not acceptable that achieving type safety can make code unsuitable for scaling. If none of your code is being used at scale then performance doesn't matter anyway.

And keep in mind that isolated tests with very little happening does not reveal the true cost of boxing. You have more heap pointers so you have more cache misses. Of course it looks like nothing when it's isolated. That's not representative of the potential cost.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Jun 23, 2018

@theunrepentantgeek you are right. It won't matter unless the code is used at scale.

Do we actually know that? Is there actually a performance problem to address here with the struct approach? Is that problem something that could be easily solved by the CLR, thus makignt his a perfectly fine solution for your needs, without any language change whatsoever?

@chrisrollins

This comment has been minimized.

chrisrollins commented Jun 23, 2018

@CyrusNajmabadi yes more heap pointers = worse performance. The CPU cache is burdened more heavily, causing more cache misses. But yeah maybe they could just improve it under the hood, idk.

Even then, would you say it's not a worthy feature? At the very least it would make it quicker and more lightweight to define these types.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Jun 23, 2018

@CyrusNajmabadi yes more heap pointers = worse performance

Why? If i wrap with a struct, i don't see why the runtime coudln't be smart enough to understnd that and treat that wrapper as effectively a no-op that just gets erased as far as passing/allocating/calling functions was concerned.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Jun 23, 2018

Even then, would you say it's not a worthy feature? At the very least it would make it quicker and more lightweight to define these types.

I don't think it's particularly slow or heavy to just create a wrapper struct :) So i'm not really big on making it that much faster...

@chrisrollins

This comment has been minimized.

chrisrollins commented Jun 23, 2018

@CyrusNajmabadi I am not sure why but various people have told me that it always boxes values inside of structs.

But anyway simple structs feel a bit smelly to me because I have to define a constructor to enable inline initialization. Although sometimes it's beneficial to include some logic in the constructor, #410 provides a really concise way to do a lot of things you'd be doing in the constructor. I really like that proposal even more than this one.

@theunrepentantgeek

This comment has been minimized.

theunrepentantgeek commented Jun 23, 2018

@chrisrollins

To me it's not acceptable that achieving type safety can make code unsuitable for scaling.

My experience is that type safety usually results in code that scales better. For example, it might enable better runtime optimization, or it might reduce code duplication, or it might permit better algorithms.

Actually, that's a good point - I've invariably found that meaningful performance gains come from better algorithms, not micro-optimizations.

@CyrusNajmabadi

Is there actually a performance problem to address here with the struct approach?

I didn't think there was. But, facts always trump opinions, so I decided to write a benchmark to find out. (BTW, I suspect this was a rhetorical question on your part as you almost certainly already know the answer.)

I created a simple project using BenchMarkDotNet and made sure to include benchmarking of allocations as well as execution time. Source code

To keep things simple, the three different approaches all just call GetHashCode() for the same string (I originally used Length the but the optimizer is just a little too clever and it managed to optimize away one of the methods completely.)

Here are the results:

Method Mean Error StdDev Median Allocated
Primitive 6.092 ns 0.2577 ns 0.5437 ns 5.875 ns 0 B
UsingStruct 5.916 ns 0.1123 ns 0.1050 ns 5.904 ns 0 B
UsingGenericStruct 5.846 ns 0.0706 ns 0.0660 ns 5.829 ns 0 B

I did all of this on my laptop, and while I was "hands off" while the benchmark was running, this isn't a machine that's been configured to eliminate as much background activity as possible. The only tuning I did was to configure power management for best performance.

Given the size of the error bars, my conclusion between these techniques is that there's no meaningful difference in performance. Crucially, they all run with zero memory allocation.

@chrisrollins I've published my source as a gist so that you can grab it and play around with the code to draw your own conclusions.

@chrisrollins

This comment has been minimized.

chrisrollins commented Jun 23, 2018

Ok perhaps I heard wrong and it does optimize away structs in certain cases.

I still think it's a good feature for making code more concise.

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Jun 23, 2018

@CyrusNajmabadi I am not sure why but various people have told me that it always boxes values inside of structs.

That would be very bad :)

@jnm2

This comment has been minimized.

Contributor

jnm2 commented Jun 23, 2018

@chrisrollins It's the other way around: storing any value type as an object or an interface type requires boxing, meaning the data is copied from its current location to a new location on the heap in order for there to be something for the object/interface instance to be referencing.

@chrisrollins

This comment has been minimized.

chrisrollins commented Jun 23, 2018

@jnm2 that makes sense. thanks.

@LeonG-ZA

This comment has been minimized.

LeonG-ZA commented Jun 30, 2018

This is a feature that I absolutely hated in C/C++. I would go through code and scratch my head just to figure out...oh this is just [some common type].
It is useful. In C/C++ it is useful for typedefing different types based on the architecture you are building on, but it often gets misused A LOT.
When I quickly browse through new code I would rather want to see the real full names of types I am familiar with.

@bondsbw

This comment has been minimized.

bondsbw commented Jun 30, 2018

IntelliSense can help. Hovering over a type identifier should show the resolved type in Quick Info.

Of course the feature could be abused. Someone could replace all the type names in their code base with acronyms or senseless names. Don't do that.

@LeonG-ZA

This comment has been minimized.

LeonG-ZA commented Jul 1, 2018

Yes, it does show up. A lot of times when I quickly scroll through new code I can get a general idea of what some class is doing, but I do have to occasionally stop. Usually it is when I encounter these:

  • var someVariable = someStuff...; //I have to hover over var or someStuff... to figure out the type.
  • I see some unknown method being used only to find out 'using static' was used so the class is hidden through a quick glance.

Of course the feature could be abused. Someone could replace all the type names in their code base with acronyms or senseless names. Don't do that.

True, I wouldn't of course. Some people just see some feature as a silver bullet that they always have to use. We once had a new guy working for us who used Tuples for EVERYTHING he touched. The code was a nightmare and had to be scrapped/rewritten. Also our fault because we didn't do code reviews at the time.

@bondsbw

This comment has been minimized.

bondsbw commented Jul 1, 2018

I suspect more was at play if the entire codebase had to be rewritten. It's straightforward to create a named type that has the same fields as a tuple.

I wouldn't be surprised if a refactoring exists to do this for you.

@jnm2

This comment has been minimized.

Contributor

jnm2 commented Jul 1, 2018

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Jul 1, 2018

The trick is to @at me three times. Then i'll write the feature for you (and also possibly haunt you).

@CyrusNajmabadi

This comment has been minimized.

CyrusNajmabadi commented Jul 1, 2018

Note: the only thing holding me back on this feature is that i need to sit down and think what it means to replace a tuple-type with a named type. Should that fix up all complimentary tuple types across your codebase? What if you do this:

(int, int) GetPair()
{
    //...
    return (x: 0, y: 1); // <- intro named type here.
}

What happens in a case like that?

Or, what if someone just uses ``(int x, int y)``` elsewhere (like even an unrelated project!), should that get updated. Should we try to just walk the usages here and only update code that is actually affected? etc. etc.

Basically, it's non-trivial to figure out what code actually needs fixing up. this is why my recent work has stuck with just converting anonymous-types. It's very clear where you can stop looking when using those guys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment