#Handling Non-Instantiated Constrained Generic Type Args in HPC#

1 messages · Page 1 of 1 (latest)

carmine snow
#

Hi,

So here's a question that I've been thinking about for the last day or so, and I still don't have a great way of working around it. It's not mission-critical, but it'd be very useful if there was a good way to do this. Hopefully I'm just missing something obvious!

I'm currently trying to build a generic struct in HPC# that works differently depending on the type parameters you select. Since there are a few parameters involved, putting them all on the generic struct seems to make the most sense to me. The generic struct also has a few copies (depending on how many generic parameters you want). In order to use the appropriate type-specific function, I've constrained the generic type to require an interface, but since some of the methods on the generic type have no arguments, I don't actually have any instances of the constrained type. So currently, I can just forcibly instantiate a default one either in a field or in a variable to call the method on it. But that obviously seems like it will allocate a rather pointless (one-byte?) struct, then throw it away? Which doesn't seem right, of course. Here's a minimal example:

public struct TestStruct<TMeta>
    where TMeta : ISomeInterface
{
    public TMeta FakeField;

    public int SomeFunction()
    {
        TMeta fakeVar = default;
        fakeVar.GetValue(); // Option 1
        return FakeField.GetValue(); // Option 2
    }
}

public interface ISomeInterface
{
    public int GetValue();
}

I do know that C# 11 would allow this through static abstract interface members, but this feature isn't available in HPC# currently.

#

My only real idea for how to resolve this currently is to just get rid of the generic parameter and create a separate struct for every implementation (and that would be multiplicative with each generic parameter) that can be passed into some set of functions that can then have an instance that way. But that seems like a very verbose way of resolving something that appears (at least on the surface) to be relatively simple.

Does anyone have any ideas on other ways to solve this?

balmy kernel
#

generally you do option 2 + constructor introduced to properly construct your generic struct

#

(if it's even required)

carmine snow
#

Yeah, for this example it's not really required, and regardless there wouldn't be anything to construct for that type parameter.

silent summit
carmine snow
# silent summit Do you see it as a kind of polymorphic structs? https://github.com/PhilSA/Polymo...

Unfortunately, not really. I've seen polymorphic structs before, and they're definitely pretty cool, but the difference here is that PhilSA's polymorphic structs are using a "type enum" in the polymorphic struct implementation. Whereas the generic-with-constrained-interface approach essentially generates code at compile time based on the generic you define.

The difference in reality is pretty minor, to be sure, and in my case this certainly could be implemented with essentially a long series of "flag" variables and a "combined" struct object that contains all possible types. Although for my purposes since I have up to ~10 generic typeargs and ~10 possible types per typearg, that would be quite a lot of code on the combined object.

That code certainly could be generated either through a source generator or some other codegen tool. Although I think the fake field is probably a lower-maintenance and still low performance impact (I mean, all of this is insignificant performance-wise, in the grand scheme of things).

#

I'm also not sure if you could combine the generic typing with the polymorphic structs. The generic typing is pretty ideal for taking in strongly-typed generic arguments. You probably could, although it might be a bit of work.

old mountain
#

I have struggled with this as well and there are a few different things I came up with over time

#

they may not be applicable to your case but perhaps they can give you some ideas

#

One of the solutions is what you described, but with a fixed-size anonymous data storage instead of generating all possible types

#

I also had a run-in with the static interface thing (basically I needed static method type constraints), I ended up using the System.Activator class to call the function I needed, thus foregoing the reliance on type safety

    public unsafe class ClassDecl<T> : IEnumerable where T : MyClass
    {
        List<MyClassBase> _childClasses = new();
        public static implicit operator T(ClassDecl<T> wrapper) { return _Create(wrapper._childClasses.ToArray()); }

        public IEnumerator GetEnumerator() { return _childClasses.GetEnumerator(); }
        public void Add(MyClassBasesystem) { _childClasses.Add(system); }

        static T _Create(params MyClassBase[] childClasses)
        {
            return Activator.CreateInstance(typeof(T), childClasses) as T;
        }
    }
#

(in this case what I really needed didn't end up having to be static)

#

So currently, I can just forcibly instantiate a default one either in a field or in a variable to call the method on it. But that obviously seems like it will allocate a rather pointless (one-byte?) struct, then throw it away?

I think creating a struct inside the function for such a purpose is fine to be honest

balmy kernel
old mountain
#

this code doesn't execute very often, burst was not a concern

you can't put constraints on the constructor arguments, so a workaround was necessary, and I started by creating a static Create() method to instantiate the classes

balmy kernel
#

Well, why not just solve this via Init interface method?

old mountain
#

because the base class is internal to another library and doesn't allow modification after construction

carmine snow
#

I do expect this code to be used potentially thousands of times per-frame, which is part of where the desire to keep it fast/safe is coming from.

As a minor update - these structs do have a random int field in them, so what I ended up going with is to just wrap that int in one of the type arguments and just require the type argument to have a getter/setter on it. Really it's just a way of "hiding" that type inside a random int field (and I use factories exclusively for constructing these, so I can just do that). Not a great workaround, but I personally prefer it over just using an unsafe approach.

carmine snow
finite moat
#

you might be able to accomplish something similar with overlapping FieldOffset's

carmine snow
#

Not sure if that was directed at me - but I don't think generic types can have explicit struct layouts?