c++templatestemplate-specialization

Template factorial function improvement


I started learning patterns in c++ and tried to create a factorial function that, if possible, will be executed in compile time. Tell me how I can improve my implementation.

Concept to check if T is numeric integral:

template <typename T>
concept NumericIntegral =
    std::same_as<int, T> || std::same_as<long, T> ||
    std::same_as<long long, T> || std::same_as<unsigned int, T> ||
    std::same_as<unsigned long, T> || std::same_as<unsigned long long, T>
    || std::same_as<short, T> || std::same_as<unsigned short, T>;

No-type template function for factorial. This function computes the factorial of a non-negative numeric integer at compile time:

template<auto val>
constexpr auto factorial(){
    static_assert(NumericIntegral<decltype(val)>, "Value must be an integral numeric type.");
    static_assert(val >= 0, "Factorial is not defined for negative numbers.");
    return val * factorial<val - 1>();
}

// Specializations for base cases n = 0
template<>
constexpr auto factorial<0>() {
    return 1;
}

// Specializations for base cases n = 1
template<>
constexpr auto factorial<1>(){
    return 1;
}

Overloaded factorial function for constexpr runtime evaluation for numeric integral types. Compiler will choose complile-time or runtime based on the constexpr context:

template<NumericIntegral T>
constexpr T factorial (T val){
    if (val < static_cast<T>(0)){
        throw std::invalid_argument("Val must be positive");
    }
    else if (val == 0 || val == 1){
        return 1;
    }
    else{
        return val * factorial(val - 1);
    }
}

// Overloaded factorial function for floating-point types at runtime. // This uses the gamma function to compute the factorial for non-integer values:

template<std::floating_point T>
T factorial(T val){
    if (val < static_cast<T>(0)){
        throw std::invalid_argument("Val must be positive");
    }
    else{
        return std::tgamma(val + 1);
    }
}

Solution

  • Don't use recursive templates unless necessary. They take time to compile, and there is a (compiler specific) limit on the depth of recursion that you will hit eventually. Also there is std::integral to require a type to be integral.

    std::integer_sequence comes in handy to avoid the recursion:

    #include <utility>
    #include <iostream>
    #include <concepts>
    
    template <typename T,T... I> requires std::integral<T>
    consteval std::size_t factorial_impl(std::integer_sequence<T,I...>) {
        return ((I+1) * ... * 1);
    }
    
    template <auto I>
    consteval std::size_t factorial() requires std::integral<decltype(I)> {
        return factorial_impl(std::make_integer_sequence<decltype(I),I>());
    }
    
    int main() {
        std::cout << factorial<5>();
    }
    

    Live Demo

    Note that std::make_integer_sequence generates a sequence [0,N), hence to compute factorial<N> I used (I+1) in the product. And * 1 is for factorial<0> when the pack is empty. consteval ensures evaluation at compile time. Note that with consteval a plain loop would do as well (https://godbolt.org/z/xPdv7b9M4).

    I suppose this is an exercise on templates. Generally when you want to save runtime by precomputing some expensive results you can also consider to compute the results, store them, and then use a lookup table or similar during runtime.