Description
Const Generics
"Const Generics" stands for allowing constant value to be used in a type parameter.
A fully working MVP implementation for CoreCLR can be found here: #89636
And an implementation including the managed part can be found here: https://github.com/hez2010/runtime/tree/feature/const-generics-managed
Link to the language proposal: dotnet/csharplang#7508
Background and Use Cases
"Const Generics" enables the use cases where developers need to pass a const value through a type parameter.
Typical use cases are templating for things like shuffle (its basically a guaranteed constant)
as well as for numerics, tensors, matrices and etc.
For example, fixed buffer and vector types [1], jagged arrays/spans [2], constrained shape of arrays [3], numeric types and multiplier types especially in graphics programming [4], expression abstractions [5], and value specialization [6].
For [1], we can have a type struct ValueArray<T, int N>
to define a type of array of T
with N
elements.
This can also be useful in variadic parameters. For example, a params ValueArray<int, 5>
can represent a variadic parameter that receives only 5 int arguments.
Beside, we can also leverage the ValueArray<T, int N>
type to implement params {ReadOnly}Span<T>
.
For [2], we can use the const type parameter to define a Span<T, int Dim>
, so we can use Span
for multi-dimension arrays as well.
For [3], we can constrain the shape of an array. This is especially useful when you are dealing with matrix or vector computations.
For example, you now can define a matrix using class Matrix<T, int Row, int Col>
. When you implement the multiplication algorithm, you can simply put a signature Matrix<T, Row, NewCol> Multiply<NewCol>(Matrix<T, Col, NewCol> rMatrix)
. This can make sure users pass the correct shape of the matrix while doing multiplication operations.
For [4], we can embed the coefficient into a multiplier type. This is especially useful in graphics programming. For example, when you are working with things about illumination, you will definitely want some multiplier types with coefficients (which are basically floating point numbers) that are guaranteed to be constants. While building AI/ML models, we are also often use such constant coefficients.
Also, we will be able to create a floating point type with user specified epsilon, such as
struct EpsilonFloating<T, T Epsilon> where T : INumber<T>
{
public static bool operator ==(EpsilonFloating<T, Epsilon> a, EpsilonFloating<T, Epsilon> b) => T.Abs(a.value - b.value) <= Epsilon;
}
and then use it like global using MyFloatWithEpsilon = EpsilonFloating<float, 1e-6f>
.
For [5], we can have several types that can embed constant values to abstract an expression, then we can validate the expression at compile time, hence no runtime exception will happen. For instance, we can have below interface types:
abstract class BinOp
sealed class AddOp : BinOp
sealed class MulOp : BinOp
interface IExpr
interface IConstExpr<T, T Value> : IExpr
interface IBinExpr<TOp, TLeftExpr, TRightExpr> where TOp : BinOp where TLeftExpr : IExpr where TRightExpr IExpr
Then we can use IBinExpr<MulOp, IBinExpr<AddOp, IConstExpr<int, 42>, IConstExpr<int, T>>, IConstExpr<int, 2>>
in a type class Foo<int T>
to represent 42 * (T + 2)
, then we can use it like a type and let the compiler to verify whether the given const type argument satisfies the expression or not.
For [6], we will be able to provide a generic Vector
type and specialize SIMD-width types with extensions:
struct Vector<T, int Size> { }
static class VectorExtension
{
public Vector<int, 4> Multiply<T>(this Vector<int, 4> v, Vector<int, 4> right) { } // Vector64
public Vector<int, 8> Multiply<T>(this Vector<int, 8> v, Vector<int, 8> right) { } // Vector128
public Vector<int, 16> Multiply<T>(this Vector<int, 16> v, Vector<int, 16> right) { } // Vector256
public Vector<int, 32> Multiply<T>(this Vector<int, 32> v, Vector<int, 32> right) { } // Vector512
public Vector<int, Size> Multiply<int Size>(this Vector<int, Size> v, Vector<int, Size> right) { } // For other sizes allowing a software fallback
// ...
public Vector<T, Size> Multiply<T, int Size>(this Vector<T, Size> v, Vector<T, Size> right) { } // For other types and sizes allowing a software fallback
}
Design
Wording
- Const type parameter: a type parameter that carries a const value.
- Const type argument: the constant value for a type parameter in the instantiation.
Const Type Parameter
⭕ This part is already implemented in the MVP implementation
New design:
To support const generics, we need a way to declare a const type parameter that will carry the const value after instantiation.
Due to the fact that a const type parameter behaves no difference than a normal type parameter until instantiation, here we can treat the type of a const type parameter as a special generic constraint.
We want to emit the type of a const type parameter as TypeSpec
, but in order to distinguish this type token from other generic constraints, we can introduce a mdtGenericParamType
and then emit the type of const type parameter with mdtGenericParamType
, and make sure it will always be the first entry in generic constraints.
To load the type of a type parameter, we simply look up the first entry in generic constraints and see if it's mdtGenericParamType
. If yes, then replace it with mdtTypeSpec
using (token & ~mdtGenericParamType) | mdtTypeSpec
. When loading generic constraints, if we see a generic constraint has type mdtGenericParamType
, we can skip it directly.
While an alternative approach (which is also the approach I preferred) is, use a type like System.Runtime.CompilerServices.LiteralType<T>
as the generic constraint, and special case it. So a class Foo<int T>
will be emitted to class Foo<T> where T : LiteralType<int>
. But in the MVP implementation I don't touch the managed libraries so I don't have the type can be used for this.
Old design:
To support const generics, we need a way to declare a const type parameter that will carry the const value after instantiation.
Due to the fact that a const type parameter behaves no difference than a normal type parameter until instantiation, here we can reuse the existing generic metadata and rules, and add a Type token to theGenericParamRec
schema. To determine whether a type parameter is a const type parameter or not, simply check the Type token to see if it's valid by usingRidFromToken
.To summarize:
Added a column to
GenericParameterRec
to save amdToken
which represents the type of a const generic parameter.
Changed the reservedDWORD
tomdToken
to save the type of a const generic parameter.
This requires a change to the existing metamodel. But worth to note that we don't need a new COM interface as we are reusing the reserved parameter inGetGenericParamProps
, bothmdToken
and the reserved DWORD are exactly DWORD.BTW: actually we have another way without upgrading the existing metadata: we can downgrade the metadata version from the current v2.0 to v1.1, where in v1.1 metadata the
GenericParamRec
table has a Kind field which is exactly what we need for const generics.
Const Type Argument
⭕ This part is already implemented in the MVP implementation
A const type argument contains the actual constant value in the instantiation.
Here we can introduce a new element type ELEMENT_TYPE_CTARG
which stands for const type argument.
A const type argument can be encoded as follows:
ELEMENT_TYPE_CTARG <element type of const value> <const value>
Note that the size of the const value is determined by its element type.
For example, an int 42
will be encoded as:
ELEMENT_TYPE_CTARG ELEMENT_TYPE_I4 42
| 1 byte | 1 byte | 4 bytes |
While a double 3.1415926
will be encoded as:
ELEMENT_TYPE_CTARG ELEMENT_TYPE_R8 3.1415926
| 1 byte | 1 byte | 8 bytes |
While we'd better to save all constants to the constant table in the metadata, then instead of inlining the const value type and const value in the signature directly, we can use the constant token in the signature which is fix-sized and easier to decode, and use the type token instead of CorElementType
so that we can also support const values of enums, int128, string and arbitrary value types as well.
IL Parser
⭕ This part is already implemented in the MVP implementation
We can reuse the keyword literal
in IL to indicate the type argument contains a const value. Particularly, we can use the keyword literal
to differentiate a const type argument/parameter from a type argument/parameter. For example, literal int32 T
.
For const type argument, we can simply use int32 (42)
to express an int constant with the value 42.
This is following the rule how we are expressing "const field" today.
We need to change the parser to parse "literal" type typeName
as a const type parameter, and type '(' value ')'
as a const type argument. You can define and use const generics as the examples at the bottom of this proposal.
Type Desc
⭕ This part is already implemented in the MVP implementation
A const type parameter has no more difference than the additional type token, so we can reuse the TypeVarTypeDesc
and add a field m_type
to save the type of const type if it's a const type parameter.
A const type argument is exactly a constant value, so we need a separate TypeDesc
for it.
Therefore, a ConstValueTypeDesc
can be added to save the type and the value of a const type argument.
We can support up to 8 bytes of constant value if we use a uint64_t
as the storage.
class ConstValueTypeDesc : TypeDesc {
TypeHandle m_type;
uint64_t m_value;
};
To read the constant value from a ConstValueTypeDesc
, we need to reinterpret the storage based on the type of constant value. For example, while reading a constant value which is a float, we can simply use *(float*)&m_value
.
Actually I'm doubting whether an uint64_t
is enough here, because we may support int128
or other types as primitive types in the future. Should we use size_t
here instead? This can make sure we are always able to save a pointer here and in case the size of size_t
is not enough for some types, we can allocate to save the value on the Non-GC heap and save its pointer to the Non-GC heap in this field:
enum {
CONST_VALUE_INLINE = 1,
CONST_VALUE_INDIRECT = 1 << 1,
};
class ConstValueTypeDesc : TypeDesc {
TypeHandle m_type;
size_t m_value;
DWORD m_flag;
};
if ((m_flag & CONST_VALUE_INDIRECT) == CONST_VALUE_INDIRECT)
{
// get size and layout info from m_type
// load the pointer from m_value
// deference the pointer to get the value
}
else
{
// get size and layout info from m_type
// load the value from m_value directly
}
Or, if we go with the constant token approach which was mentioned in the "Const Type Argument" section, we may simply use the token of constant value instead:
class ConstValueTypeDesc : TypeDesc {
TypeHandle m_type;
mdToken m_value;
};
But this soon brings another issue where making a new const value type using reflection APIs will create a new constant record that is not present in the metadata.
Method Table
⭕ This part is already implemented in the MVP implementation
Similar to function pointers, we don't need a MethodTable
for const value.
Type Loader
⭕ This part is already implemented in the MVP implementation
We can always load constant values in the CoreLib module because a constant value is independent from the assembly, the same constant value can be served from any assembly.
To avoid loading the same constant value other than once, once we load a constant value, we can save it into a hash table m_pAvailableParamTypes
.
Whenever we load a constant value, we first lookup in the hash table, if found then we load the TypeHandle
from the hash table directly, otherwise we allocate a new ConstValueTypeDesc
for it.
Value Loading
⭕ This part is already implemented in the MVP implementation
We need to use the const value from a type parameter, here we can reuse the ldtoken
instruction to achieve this.
Instead of loading the TypeHandle
of the type parameter, we need to load the constant value and push it to the stack directly when we see the type parameter is a const type parameter.
JIT
⭕ This part is already implemented in the MVP implementation
We only need to handle ldtoken
here, so we can change the impResolveToken
to resolve the information about the const value as well, and then use the information to determine whether we should load a type handle or a const value to the stack. So we only need a minor necessary change in the importation phase.
Further changes would probably necessary after we introduce types like Vector<T, int Length>
, as the JIT needs to recognize it to allow hardware acceleration.
Generic Sharing
⭕ This part is already implemented in the MVP implementation
We don't share the implementation among const generic type parameters. Each const type argument gets specialized so we can always import the const type argument as a real type-rich constant value anytime.
Type Unloadability
⭕ This part is already implemented in the MVP implementation
They are just constant values and can be reused by any other assemblies, so we don't need to unload them at all.
Type Validation
⭕ This part is already implemented in the MVP implementation
We need to validate whether the const value type can be passed to a const type parameter.
We can do it during checking the generic constraints: whenever we meet a const value, we can simply check whether the const value type is equivalent to the type saved in generic param props.
Alternatively, we can also do it at the token resolution.
Generic on Const Generic Type Parameter
⭕ This part is already implemented in the MVP implementation
We can also support generic type on a const generic type parameter.
For example,
.class public auto ansi beforefieldinit Test`2<T, literal !T N>
{
.method public hidebysig newslot virtual
instance void M<U, literal !!U V> () cil managed
{ }
}
Here we can leverage the type
field in the GenericParamRec
to save a type spec, then we will be able to look up the type parameter.
This will allow us to write something like struct ValueArray<T, TSize, literal TSize Size>
and use it with ValueArray<int, int, 42424242>
, ValueArray<int, long, 42424242424242>
, and etc.
Also we can leverage this feature to define a ConstValueExpression<TValue, TValue Value>
and use it while implementing a compiler/interpreter.
Overloading
❌ This part is NOT yet implemented in the MVP implementation
🚧 This part still needs more discussions to reach a conclusion
In this design, we are differentiating the calling target at the call site, so we can support overloading on const generic type parameters without any issues.
call instance void Foo`1<float32 (42.42)>::.ctor(); // instantiate the Foo`1<float32 (42.42)>
call instance void Foo`1<int32 (42)>::.ctor(); // instantiate the Foo`1<int32 (42)>
call instance void Foo`1<int32 (42)>::A<int32 (42)>(); // calling the Foo`1<int32 (42)>::A<int32 (42)>()
call instance void Foo`1<int32 (42)>::A<float32 (42.42)>(); // calling the Foo`1<int32 (42)>::A<float32 (42.42)>()
.class public auto ansi beforefieldinit Foo`1<literal int32 N>
{
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed { ... }
.method public hidebysig newslot virtual
instance void A<literal int32 X>() cil managed { ... }
.method public hidebysig newslot virtual
instance void A<literal float32 X>() cil managed { ... }
}
.class public auto ansi beforefieldinit Foo`1<literal float32 N>
{
// ...
}
This would require us to consider the type of a type parameter while resolving tokens, i.e., making the type of a const type parameter part of the signature. We need to decide whether to support it or not before we are actually shipping const generics, because once we ship const generics, we can't afford a breaking change around signature encoding.
While given the fact that we can support generics on const generic type parameter, the overloading support is not so much necessary IMO.
Constraints
❌ This part is NOT yet implemented in the MVP implementation
It's useful to constraint a const type parameter. For example, the dimension of a nd-Span ref struct Span<T, int Dimension>
should not be less than 1, and the length of a struct ValueArray<T, int Length>
should not be less than 0.
We can add the below APIs to achieve arithmetic constraints.
namespace System.Runtime.CompilerServices;
public abstract class Operator
{
public abstract class UnaryOperator : Operator
{
// ...
}
public abstract class BinaryOperator : Operator
{
public sealed class AdditionOperator : BinaryOperator { }
public sealed class SubtractionOperator : BinaryOperator { }
public sealed class MultiplyOperator : BinaryOperator { }
public sealed class DivisionOperator : BinaryOperator { }
public sealed class EqualityOperator : BinaryOperator { }
public sealed class LessThanOperator : BinaryOperator { }
public sealed class ConjunctionOperator : BinaryOperator { }
public sealed class DisjunctionOperator : BinaryOperator { }
// ...
}
}
public interface IExpression
{
public interface IUnaryExpression<TOperator, TOprand> : IExpression where TOperand : IExpression where TOperator : UnaryOperator { }
public interface IBinaryExpression<TOperator, TLeft, TRight> : IExpression where TLeft : IExpression where TRight : IExpression where TOperator : BinaryOperator { }
public interface IConstantExpression<TValue, TValue Value> : IExpression { }
...
}
Then we can evaluate the expression when we validate the generic constraints. For example, to constraint N
to be greater than 0 and less than 20, we can use:
class Foo<int T> where T : > 0, < 20 { }
And this got lowered to:
.class public auto ansi beforefieldinit Foo<literal int32 (
class BinaryExpression`3<
class Operator/BinaryOperator/GreaterThanOperator,
class IExpression/IConstantExpression`2<int32, !!T>,
class IExpression/IConstantExpression`2<int32, int32 (0)>
>,
class BinaryExpression`3<
class Operator/BinaryOperator/LessThanOperator,
class IExpression/IConstantExpression`2<int32, !!T>,
class IExpression/IConstantExpression`2<int32, int32 (20)>
>
) T>
extends [System.Runtime]System.Object { }
I have a naive prototype commit in another branch for show case only: hez2010@e1fa0c3
However, those expression types are actually not being implemented by any types, but we still use them in the generic constraints which let them look like interface constraints but behave as expression evaluation, which is not intuitive.
For example, we can add something like constexpr
constraints in the metadata and allow it to be emitted directly, so class Foo<T, U, V> where V : == T + U where T : != 0
can be represented in IL as:
.class public auto ansi beforefieldinit Foo<literal int32 (constexpr (!T != int32 (0))) T, literal int32 U, literal int32 (constexpr (!V == !T + !U)) V>
Const Arithmetic
❌ This part is NOT yet implemented in the MVP implementation
🚧 This part still needs more discussions to reach a conclusion
It's useful to have arithmetic support for const generics.
For example, the signature of a Push
method of ValueArray<T, int N>
type can be ValueArray<T, N + 1> Push(T elem)
, and the signature of a Concat
method can be ValueArray<T, N + M> Concat<int M>(ValueArray<T, M> elems)
.
This would require embedding the arithmetic operations in the type and implementing dependent/associated types, which is a non-trivial work.
While an alternative is to use constraints to achieve it. So for the example of Push
method, we can use ValueArray<T, U> Push<int U>(T elem) where U : (T + 1)
, and the constraint T + 1
can be expressed using IBinaryExpression<Add, IConstantExpression<int, T>, IConstantExpression<int, 1>>
. Then we can validate the constraint at runtime.
Although we need to specify the value such as Push<7>(42)
while calling on ValueArray<int, 6>
, the C# compiler may automatically infer the type of U
so developers don't have to explicitly specify the value of U
every time.
However, consider the below code:
class Foo<int T>
{
private Foo<T + 1> foo;
}
Are we going to enforce users to introduce a new type parameter on Foo
? I.e.,
class Foo<int T, int U> where ...
{
private Foo<U> foo;
}
If yes, whenever we want to introduce a new "computed" const type parameter on a method of the class, we will need to add it to the class signature, which will lead to breaking changes. This seems quite unfortunate, and unacceptable.
Therefore, we cannot just rely on generic constraints to serve const arithmetic.
However, if we have runtime support for dependent/associated types in the future, this can be simply resolved by using:
class Foo<int T>
{
type N = T + 1;
private Foo<N> foo;
}
And also, if we have the support for defining an associated type inside a method, we can do:
class Foo
{
U Method<int T>()
{
type U = T + 1;
}
}
We still need some discussion to design around here.
Maybe we can just skip const arithmetic for the first version, and implement const arithmetic in the future once we have proper runtime support?
Built-in ValueArray
Intrinsic Type
❗ The implementation can be found here, though this part is not included in the MVP implementation
We need a built-in ValueArray
, aka. FixedBuffer
type for use, and it will play an important role in public APIs. A ValueArray
is basically the InlineArray
we already have today plus the ability to specify arbitrary length without the need to define a new InlineArray
type.
Below is the dummy C# code for ValueArray
:
struct ValueArray<T, int N>
{
private T elem; // Repeat the field elem for N times
public int Length { get; } // ldtoken !N; ret;
public ref T this[int index] { ... }
}
This can be used together with params
:
Foo(1, 2, 3, 4, 5);
// a method that only receives 5 int arguments
void Foo(params ValueArray<int, 5> args) { }
Particularly, in C# we can lower all fixed buffer types to ValueArray
, and it can perfectly serve all features like params Span<T>
and stackalloc T[]
.
Reflection APIs
❗ The implementation can be found here, though this part is not included in the MVP implementation
To support reflection, we need something like MakeGenericType
for a const value as well, so I have the below API proposal:
namespace System;
public abstract class Type
{
public virtual bool IsConstValue { get; }
public virtual object ConstValue { get; }
public static Type MakeConstValueType(object value);
}
This can make sure we can instantiate a type/method that contains const type parameters, and also get the const value from a constructed type argument.
Some use patterns of reflection:
class Foo<T, int N> { }
var foo = new Foo<string, 42>();
foo.GetType(); // Foo<string, int (42)>
foo.GetType().GetGenericArguments()[0]; // Type: System.String
foo.GetType().GetGenericArguments()[1].IsConstValue; // true
foo.GetType().GetGenericArguments()[1].HasElementType; // true
foo.GetType().GetGenericArguments()[1].ConstValue; // 42
foo.GetType().GetGenericArguments()[1].GetElementType(); // System.Int32
var t = Type.MakeConstValue(42);
var d = typeof(Foo<,>);
d.GetGenericArguments()[1].IsConstValue; // false
d.GetGenericArguments()[1].HasElementType; // true
d.GetGenericArguments()[1].ConstValue; // InvalidOperationException
d.GetGenericArguments()[1].GetElementType(); // Type: System.Int32
d.MakeGenericType(typeof(string), t); // Foo<string, int (42)>
An interesting idea is to allow typeof(value)
for the Type.MakeConstValue
, for example, typeof(42)
to get a Type
that contains a value 42
.
This would either require us to:
- Use the
ldtoken
instruction for this, and we will need to introduce a new instruction for loading a const type argument to the stack, for example, an instruction calledldctarg
(load const type argument). - Introduce a new instruction for this.
- No new instruction, and just compile it to
Type.MakeConstValue
.
Changes to ECMA-335
Basically the new element type ELEMENT_TYPE_CTARG
.
Compatibility Concerns
Tooling
Disassembler
Both ILSpy and dnSpy should able to special case the mdtGenericParamType
while loading generic constraints.
Profilers and Debuggers
They need to support decoding new types or methods which contain ELEMENT_TYPE_CTARG
/CORINFO_TYPE_CTARG
on the signature.
As for debuggers, they need to add support for the extended ldtoken
instruction.
EnC
We don't support modifying generic type signatures today, so no actions are needed.
Other 3rd Party Tools
With the new design, we are not breaking the metadata so no concern here.
Other Useful APIs
Other many APIs can make use of const generics to provide valuable features and abilities for users:
Matrix<T, int Row, int Col>
: fixed-sized matrix to supersedeMatrix3x3
,Matrix4x4
and etc.Vector<T, int N>
: fixed-sized vector to supersedeVector2
,Vector3
and etc.Tensor<T, int Rank>
: tensor types for AI/ML purposeSpan<T, int Dim>
: ND-span that can support multiple dimension arraysList<T, int N>
,Array<T, int N>
...: arbitrary list types can have a fixed size now- ... and more
Future Considerations
Support for Strings and Arbitrary Value Types
This can be done by changing the parser to allow strings and arbitrary value types as well.
For example,
// value types
.class C`1<literal valuetype Foo T> { }
call C`1<valuetype Foo (bytearray ( 01 00 00 00 02 00 00 00 03 00 00 00))>::.ctor()
// string
.class D`1<literal string T> { }
call D`1<string ("hello world")>::.ctor()
where Foo
is a Vector3<int>
, so we are passing a Vector3<int> { X = 1, Y = 2, Z = 3 }
here.
And as for the implementation, we can use the m_type
in ConstValueTypeDesc
to save the TypeHandle
of the type, and m_value
to save the address or constant record token. In this way, we can extend Const Generics to strings and arbitrary value types as well.
We only need to extend the encoding of const type arguments as following:
- For strings, we encode the binary following the rule:
ELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <constant record token>
, orELEMENT_TYPE_CTARG ELEMENT_TYPE_STRING <length> <qcompString>
- For arbitrary value types:
ELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <constant record token>
, orELEMENT_TYPE_CTARG ELEMENT_TYPE_VALUETYPE <compressed type token> <length> <bytearray>
This won't be a breaking change so we can do this later.
Fully Working Prototype
This prototype is based on the old design with a breaking change to the metadata, while the latest (current) design doesn't have any breaking changes to the metadata
I have done the fully working prototype of C# compiler, language server and CoreCLR runtime, and successfully built a SDK for it (Windows only).
If you want to have a try on const generics, you can download the SDK here: https://1drv.ms/u/s!ApWNk8G_rszRgrxP32IMKhW-V8iWug?e=JBn8wU
Be sure to follow the README.txt in the SDK.
Version: 20230912 Build 1
Checksum: a8c9ee29d1accd14797f60bedced312f9524391b
This prototype branch:
- Roslyn: https://github.com/hez2010/roslyn/tree/feature/const-generics
- Runtime: https://github.com/hez2010/runtime/tree/feature/const-generics-managed
I may update the SDK without posting a new comment but change the version and checksum in the above, while the sharing link won't change.
This prototype supports all things in this proposal except generic constraints on const type parameter and const arithmetic.
For example, you can do the following things:
- Declare a const generic type, eg.
class Foo<T, int N>
. - Use a const generic type, eg.
new Foo<int, 42>()
. - Declare a const generic method, eg.
void Foo<int X>
. - Use a const generic method, eg.
Foo<42>()
. - Generics on const type parameter, eg.
class Foo<T, T X>
, then you can use it withFoo<int, 42>
as well asFoo<float, 42.42424f>
. - Use const type parameter as constant directly. eg. calling
Console.WriteLine(X)
in the typeclass Foo<int X>
. typeof
support. eg.typeof(42)
.- Casting support in const type argument. eg.
new Foo<(short)42>
,typeof((short)42)
- A built-in value type
ValueArray<T, int X>
that can be used as a fix-sized type with typeT
and lengthX
. - A niche syntax for declaring a
ValueArray
type, eg.int[42]
. - Full reflection support.
- To check whether a type parameter is const type parameter, use
type.IsGenericParameter && type.HasElementType
. - To get the type of a const type parameter, use
type.GetElementType()
. - To check whether a type argument is const type argument, use
type.IsConstValue
. - To get the type of a const type argument, use
type.GetElementType()
. - To get the value of a const type argument, use
type.ConstValue
. - To make a const value type, use
Type.MakeConstValueType()
- To check whether a type parameter is const type parameter, use
Code Examples
A basic example
.assembly _ {}
.class public auto ansi beforefieldinit Foo`2<T, literal int32 N>
extends [System.Runtime]System.Object
{
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ret
}
.method public hidebysig newslot virtual
instance void M<literal int32 V, literal int32 W> () cil managed
{
.maxstack 1
.locals init (
[0] int32 v
)
newobj instance void class Foo`2<string, int32 (42)>::.ctor()
call instance void class Foo`2<string, int32 (42)>::M<!!V, !!V>()
newobj instance void class Foo`2<string, !!V>::.ctor()
call instance void class Foo`2<string, !!V>::M<!N, int32 (42)>()
newobj instance void class Foo`2<string, !N>::.ctor()
call instance void class Foo`2<string, !N>::M<!!V, !!W>()
ldtoken !!V
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !!W
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !N
call void [System.Console]System.Console::WriteLine(int32)
ret
}
}
This can be interpreted to the following dummy C# code:
class Foo<T, int N>
{
public void M<int V, int W>()
{
new Foo<string, 42>().M<V, V>();
new Foo<string, V>().M<N, 42>();
new Foo<string, N>().M<V, W>();
Console.WriteLine(V);
Console.WriteLine(W);
Console.WriteLine(N);
}
}
Generic Virtual Method with Const Type Parameters
.assembly _ {}
.class private auto ansi beforefieldinit Program
extends [System.Runtime]System.Object
{
.method private hidebysig static
void Main (
string[] args
) cil managed
{
.maxstack 8
.entrypoint
newobj instance void class Bar`2<string, int32( 42 )>::.ctor()
call instance void class Bar`2<string, int32( 42 )>::N<int32( 42 ), int32( 42 )>()
ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ret
}
}
.class public auto ansi beforefieldinit Foo`2<T, literal int32 N>
extends [System.Runtime]System.Object
{
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ret
}
.method public hidebysig newslot virtual
instance void M<literal int32 V, literal int32 W> () cil managed
{
.maxstack 8
ldstr "From Foo::M"
call void [System.Console]System.Console::WriteLine(string)
ldtoken !!V
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !!W
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !N
call void [System.Console]System.Console::WriteLine(int32)
ret
}
.method public hidebysig newslot virtual
instance void N<literal int32 V, literal int32 W> () cil managed
{
.maxstack 8
newobj instance void class Foo`2<string, int32( 42 )>::.ctor()
call instance void class Foo`2<string, int32 (42)>::M<!!V, !!V>()
newobj instance void class Foo`2<string, !!V>::.ctor()
call instance void class Foo`2<string, !!V>::M<!N, int32 (42)>()
newobj instance void class Foo`2<string, !N>::.ctor()
call instance void class Foo`2<string, !N>::M<!!V, !!W>()
ret
}
}
.class public auto ansi beforefieldinit Bar`2<T, literal int32 N>
extends class Foo`2<!T, int32 (128)>
{
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void class Foo`2<!T, int32 (128)>::.ctor()
ret
}
.method public hidebysig virtual
instance void M<literal int32 V, literal int32 W> () cil managed
{
.maxstack 8
.locals init (
[0] string v
)
ldstr "From Bar::M"
call void [System.Console]System.Console::WriteLine(string)
ldtoken !!V
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !!W
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !N
call void [System.Console]System.Console::WriteLine(int32)
ret
}
.method public hidebysig virtual
instance void N<literal int32 V, literal int32 W> () cil managed
{
.maxstack 8
ldarg.0
call instance void class Foo`2<!T, int32 (128)>::M<!!V, !!W>()
ldarg.0
callvirt instance void class Foo`2<!T, !N>::M<!!V, !!W>()
ret
}
}
This will yield the below execution result:
From Foo::M
42
42
128
From Bar::M
42
42
42
Generic Virtual Method with Generic on Const Type Parameters
.assembly _ { }
.class private auto ansi beforefieldinit Program
extends [System.Runtime]System.Object
{
.method private hidebysig static
void Main (
string[] args
) cil managed
{
.maxstack 8
.entrypoint
newobj instance void class Bar`2<float32, int32( 42 )>::.ctor()
call instance void class Bar`2<float32, int32( 42 )>::N<float32( 42.42 ), int32( 42 )>()
ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ret
}
}
.class public auto ansi beforefieldinit Foo`2<T, literal int32 N>
extends [System.Runtime]System.Object
{
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ret
}
.method public hidebysig newslot virtual
instance void M<literal !T V, literal int32 W> () cil managed
{
.maxstack 8
ldstr "From Foo::M"
call void [System.Console]System.Console::WriteLine(string)
ldtoken !!V
box !T
call void [System.Console]System.Console::WriteLine(object)
ldtoken !!W
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !N
call void [System.Console]System.Console::WriteLine(int32)
ret
}
.method public hidebysig newslot virtual
instance void N<literal !T V, literal int32 W> () cil managed
{
.maxstack 8
newobj instance void class Foo`2<int32, int32( 42 )>::.ctor()
call instance void class Foo`2<int32, int32 (42)>::M<!!V, !!V>()
newobj instance void class Foo`2<int32, !!V>::.ctor()
call instance void class Foo`2<int32, !!V>::M<!N, int32 (42)>()
newobj instance void class Foo`2<int32, !N>::.ctor()
call instance void class Foo`2<int32, !N>::M<!!V, !!W>()
ret
}
}
.class public auto ansi beforefieldinit Bar`2<T, literal int32 N>
extends class Foo`2<!T, int32 (128)>
{
.method public hidebysig specialname rtspecialname
instance void .ctor () cil managed
{
.maxstack 8
ldarg.0
call instance void class Foo`2<!T, int32 (128)>::.ctor()
ret
}
.method public hidebysig virtual
instance void M<literal !T V, literal int32 W> () cil managed
{
.maxstack 8
.locals init (
[0] string v
)
ldstr "From Bar::M"
call void [System.Console]System.Console::WriteLine(string)
ldtoken !!V
box !T
call void [System.Console]System.Console::WriteLine(object)
ldtoken !!W
call void [System.Console]System.Console::WriteLine(int32)
ldtoken !N
call void [System.Console]System.Console::WriteLine(int32)
ret
}
.method public hidebysig virtual
instance void N<literal !T V, literal int32 W> () cil managed
{
.maxstack 8
ldarg.0
call instance void class Foo`2<!T, int32 (128)>::M<!!V, !!W>()
ldarg.0
callvirt instance void class Foo`2<!T, !N>::M<!!V, !!W>()
ret
}
}
This will yield the below execution result:
From Foo::M
42.42
42
128
From Bar::M
42.42
42
42
Minimal ValueArray Type Implementation
.class public sequential ansi sealed beforefieldinit System.ValueArray`2<T, literal int32 Length>
extends [System.Runtime]System.ValueType
{
.field private !T elem
.method public hidebysig specialname instance !T& get_Item (int32 index) cil managed
{
.custom instance void [System.Runtime]System.Diagnostics.CodeAnalysis.UnscopedRefAttribute::.ctor() = (01 00 00 00)
.maxstack 8
ldarg.1
ldc.i4.0
blt.s OutOfRange
ldarg.1
ldarg.0
call instance int32 valuetype System.ValueArray`2<!T, !Length>::get_Length()
blt.s GetItem
OutOfRange:
call void valuetype System.ValueArray`2<!T, !Length>::ThrowIndexOutOfRange()
GetItem:
ldarg.0
ldflda !0 valuetype System.ValueArray`2<!T, !Length>::elem
ldarg.1
call !!0& [System.Runtime]System.Runtime.CompilerServices.Unsafe::Add<!T>(!!0&, int32)
ret
}
.method public hidebysig specialname instance int32 get_Length () cil managed
{
.maxstack 8
ldtoken !Length
ret
}
.method private hidebysig static void ThrowIndexOutOfRange () cil managed
{
.maxstack 8
newobj instance void [System.Runtime]System.IndexOutOfRangeException::.ctor()
throw
}
.property instance !T& Item(int32 index)
{
.get instance !0& System.ValueArray`2::get_Item(int32)
}
.property instance int32 Length()
{
.get instance int32 System.ValueArray`2::get_Length()
}
}