constexpr vs C++ Template Metaprogramming

Last time i was experimenting with what constexpr has to offer for compile time calculations & ran into limits with how nested these can be. However back then with C++11 these could have only 1 return statement and thus were good but anything fancy would have resulted in convulated implementation.

With much better C++17 compiler support quite a few of the template metaprograms can easily be moved to constexpr. Esp the implementation from here and here becomes just a few functions rather than #n types.

factorial is as simple as (but still have just 1 return statement, so C++11 be fine):

1
2
3
4
constexpr int factorial (int num) 
{
    return num < 2 ? 1 : num * factorial (num - 1);
}

instead of

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template<int Num>
struct factorial_c
{
enum
    {
        value = Num * factorial_c<Num-1>::value
    };
};

//  terminating conditions
//  factorial of 1 is 1
template<>
struct factorial_c<1>
{
    enum
    {
        value = 1
    };
};

//  factorial of 0 is 1
template<>
struct factorial_c<0>
{
    enum
    {
        value = 1
    };
};

But the biggest difference is with C++17:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int minimum (int a, int b) 
{
    return a < b ? a : b;
}

int maximum (int a, int b) 
{
    return a > b ? a : b;
}

int gcd_recursive(int a, int b)
{
    if( a == b ) return a;    
    if( minimum(a,b) == 0 ) return maximum(a,b);    
    return gcd_recursive(minimum (a, b), maximum (a, b) % minimum (a, b));
}

becomes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
constexpr int minimum (int a, int b) 
{
    return a < b ? a : b;
}

constexpr int maximum (int a, int b) 
{
    return a > b ? a : b;
}

constexpr int gcd (int a, int b) 
{
    if (a == b) return a;
    if (minimum (a, b) == 0) return maximum (a, b);
    return gcd (minimum (a, b), maximum (a, b) % minimum (a, b));
}

instead of:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
template<int One, int Two>
struct  max_c
{
    enum
    {
        value = One > Two ? One : Two
    };
};

template<int One, int Two>
struct min_c
{
    enum
    {
        value = One < Two ? One : Two
    };
};

template<int One, int Two>
struct gcd_c
{
    enum
    {
        //  equivalent to: `value = gcd(min, max % min)`
        value = gcd_c<  min_c<One, Two>::value,
                        max_c<One, Two>::value %
                        min_c<One,Two>::value>::value
    };
};

//  template specialization for Min being 0, this is our
//  termination condition for recursion
template<int Max>
struct gcd_c<Max, 0>
{
    enum
    {
        value = Max
    };
};


And these are all computed at compile-time as is evident from the program here

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int main () 
{
    constexpr auto fac5 = factorial (5);
        
    constexpr auto gcdval = gcd (17 * 17, 17 * 16);
    static_assert(gcdval == 17, "incorrect gcd value");

    constexpr auto lcmval = lcm(17*17, 17*16);
    static_assert(lcmval == 4624, "incorrect lcm value");
          
    return 0;
}


generated assembly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fac5$ = 0
gcdval$ = 4
lcmval$ = 8
main    PROC
$LN3:
        sub     rsp, 24
        mov     DWORD PTR fac5$[rsp], 120   ; 00000078H
        mov     DWORD PTR gcdval$[rsp], 17
        mov     DWORD PTR lcmval$[rsp], 4624          ; 00001210H
        xor     eax, eax
        add     rsp, 24
        ret     0
main    ENDP