QuantumC generics allow types and functions to work with multiple different types while keeping compile-time type safety.
Generics are available on:
Generics are Monomorphised
A generic type is declared using angle brackets with the type-names in the center:
class Box<T> {
T value;
Box(T value) {
this->value = value;
}
}
int main() {
Box<int> box = Box<int>(123);
}
Another example:
class Pair<A, B> {
A first;
B second;
Pair(A first, B second) {
this->first = first;
this->second = second;
}
}
Pair<int, string> p = Pair<int, string>(123, "hello");
Generic parameters can have constraints to restrict what types are allowed.
The syntax is:
<T([constraint]:[[!]<subconstraints>])>
For example:
class NumberBox<T(numeric:)> {
T value;
}
This means T must be a numeric type.
Available built-in constraints include:
| Constraint | Meaning |
|---|---|
usertype |
Any user-defined type |
primitive |
Any primitive type |
pointer |
Any pointer type |
numeric |
Any numeric type |
Generic constraints can also have subconstraints, which either restrict allowed types or exclude specific types. Syntax:
<T(:!Type)>
The ! means “not this type”.
Example:
class NotInt<T(:!int)> {
T value;
}
This allows any type except int.
Multiple types can be included or excluded using |.
Example:
<T(:!int|string)>
means:
T cannot be int or string.
Constraints and exclusions can be combined. Example:
<T(numeric:!int|float)>
This means:
T must be numericT cannot be intT cannot be floatQuantumC also supports generic parameters that are values known at compile time.
Example:
class Array<T, int Size> {
T data[Size];
}
Size is not a type. It is a compile-time integer parameter.
Usage:
Array<int, 32> numbers;
The compiler knows the size during compilation.
Functions can also use generics.
Example:
T max<T(numeric:)>(T a, T b) {
if (a > b) {
return a;
}
return b;
}
Usage:
int x = max<int>(10, 20);
double y = max<double>(1.5, 2.5);
Because QuantumC prioritizes explicitness, generic parameters are not inferred. You must provide them manually.
Methods can have generic parameters independently from their class. Example:
class Printer {
T print<T>(T value) {
`qout("%s", value);
return value;
}
}
Aliases can also use generics. Example:
type Pointer<T> = T*;
Pointer<int> x;
This creates:
int* x;
QuantumC generics use constraint-based typing. Other languages often express simple requirements using large collections of traits, concepts, or template checks. For example, a numeric constraint in Rust may require listing many traits:
T: Add + Sub + Mul + Div + PartialOrd + Copy
In QuantumC:
T(numeric:)
expresses the intent directly. The compiler understands what “numeric” means instead of requiring the programmer to manually describe every required operation.
By convention, generic parameters use short uppercase names:
class Array<T, int Size>
Common names:
| Name | Meaning |
|---|---|
T |
General type |
A, B |
Additional types |
K |
Key type |
V |
Value type |
S, Size |
Compile-time size |
However, generic parameter names are normal identifiers and follow the same rules as other names. Specifically, generic parameters may use either the constant or usertype casing rules.
class Array<T, int S = 0> {
T* data;
int size;
Array() {
this->data = nullptr;
this->size = 0;
}
T get(int index) {
return this->data[index];
}
}
int main() {
Array<int, 10> numbers;
Array<string> names;
}
In Rust,
<T(numeric:)>
Would be
<T: std::ops::Add<Output = T> + std::ops::Sub<Output = T> + std::ops::Mul<Output = T> + std::ops::Div<Output = T> + PartialOrd + Copy>
And in C++, it would be
template <typename T>
requires std::is_arithmetic_v<T>
T
or in old SFINAE C++
template <typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>>
T
And in Zig:
...(comptime T: type, ...) ... {
switch (@typeInfo(T)) {
.Int, .Float => ...,
else => @compileError("T must be numeric"),
}
}