One usage pattern of const generics


#1

This is a basic usage of “const generics” (value template arguments) in D language with a constraint:

uint sum1(size_t N)(const ref uint[N] arr)
if (N > 0 && N % 3 == 0) {
    uint result = 0;
    foreach (const x; arr)
        result += x;
    return result;
}
void main() {
    import std.stdio;
    uint[3] a = [10, 20, 30];
    writeln(sum1(a));
    uint[6] b = [10, 20, 30, 40, 50, 60];
    writeln(sum1(b));
}

(For simplicity I have not annotated the sum1 function with @safe pure nothrow @nogc). The sum1() function accepts as input constant references of fixed-size arrays of u32 values as long as their compile-time length N satisfies the predicate:

N > 0 && N % 3 == 0

This sum1() code is simple and very readable but the compiler instantiates one different sum1() function for each different N, this sometimes increases the performance of the code (if N is known at compile-time and it’s small, the back-end of the compiler could unroll the loop and produce very efficient code) but also could bloat your binary and add pressure on the CPU code cache. To avoid the bloat D programmers sometimes use just the dynamic slice version, with run-time contracts:

uint sum2(const uint[] arr)
in {
    assert(arr.length > 0 && arr.length % 3 == 0);
} body {
    uint result = 0;
    foreach (const x; arr)
        result += x;
    return result;
}

But this replaces the compile-time enforcement of the array length with a run-time test. To keep the compile-time test and avoid most bloat D programmers sometimes write something like this:

private uint sum3_helper(const uint[] arr) {
    uint result = 0;
    foreach (const x; arr)
        result += x;
    return result;
}
uint sum3(size_t N)(const ref uint[N] arr)
if (N > 0 && N % 3 == 0) {
    return sum3_helper(arr[]);
}
void main() {
    import std.stdio;
    uint[3] a = [10, 20, 30];
    writeln(sum3(a));
    uint[6] b = [10, 20, 30, 40, 50, 60];
    writeln(sum3(b));
}

sum3() is used to perform just the compile-time enforcement of the constaint, it’s very short because it’s just a call to the sum3_helper() function that uses run-time-sized slices. So in the binary there’s only one sum3_helper() plus the tiny sum3() stubs.

In Rust const generics I’d like some built-in support for that coding pattern, generic functions that have constant compile-time parameters, but if the programmer desires so the constant values are ghost constants, that is purely compile-time constraints that don’t induce separate instantiations of the code.