Is it possible to derive template parameters from a literal integer in a formula somehow?

104 Views Asked by At

I have a class that wraps an integer into a range of values known only to the compiler (and the developer), the limits are unknown at runtime. The class implements operators such that the limits change and a new value of a new type with the modified limits is returned (this is crucial).

The following code gives an example of the class, implementing the + operator as described (compiles for c++20):

#include <iostream>
using namespace std;

template< int LOWER_, int UPPER_ >
class MyRange final {
public:
    constexpr static int LOWER = LOWER_;
    constexpr static int UPPER = UPPER_;

    template< int, int >
    friend class MyRange;

    constexpr MyRange(MyRange const &) noexcept = default;
    constexpr MyRange(MyRange&&) noexcept = default;
    constexpr ~MyRange() {}

    template< int VALUE >
    requires ( VALUE >= LOWER && VALUE <= UPPER )
    static constexpr
    MyRange wrap = MyRange( VALUE );

    template< class _RHS, int _RHS_LOWER = _RHS::LOWER, int _RHS_UPPER = _RHS::UPPER,
        int _RES_LOWER = LOWER + _RHS_LOWER, int _RES_UPPER = UPPER + _RHS_UPPER,
        typename _RESULT_T = MyRange<_RES_LOWER, _RES_UPPER> >
    friend
    _RESULT_T
    operator+(MyRange const lhs, _RHS const &rhs) noexcept {
        int result = lhs.value + rhs.unwrap();
        return construct<_RESULT_T>( result );
    }

    int unwrap() const noexcept { return value; }

private:
    MyRange() = delete;
    MyRange& operator=(MyRange const &) = delete;
    MyRange& operator=(MyRange&&) = delete;

    // this must not be exposed because value has to be checked against limits at compile-time;
    // wrap<VALUE> is the public "constructor"
    explicit constexpr MyRange(int value) noexcept : value(value) {}

    // helper: construct another specialization of MyRange
    template< class TO >
    static constexpr TO construct(int value) noexcept { return TO(value); }

    int const value;
};

How this can be used:

int main() {
    auto value = MyRange<5,20>::wrap<8>;
    auto another = MyRange<6,10>::wrap<6>;
    auto result = value + another;

    // 14; limits: 11, 30
    cout << result.unwrap() << "; limits: " << decltype(result)::LOWER << ", " << decltype(result)::UPPER << endl;
}

Now I have the following problem. I would like to be able to add integer literals to variables of the range class type. This could be achieved with explicit or implicit conversion, however this would make the limits explode unnecessarily:

    using Range = MyRange<5,20>;
    auto value = Range::wrap<8>;

    auto result = value + Range::wrap<6>;

    // 14; limits: 10, 40
    cout << result.unwrap() << "; limits: " << decltype(result)::LOWER << ", " << decltype(result)::UPPER << endl;

Of course I could wrap the literal integer explicitly to obtain the desired result:

    auto value = MyRange<5,20>::wrap<8>;
    auto result = value + MyRange<6,6>::wrap<6>;

    // 14; limits: 11, 26
    cout << result.unwrap() << "; limits: " << decltype(result)::LOWER << ", " << decltype(result)::UPPER << endl;

But I don't like it. Too much overhead on the user side. I would rather prefer to write something like auto result = value + 6; and the integer literal 6 is converted implicitly to MyRange<6,6>::wrap<6> before it is passed to the operator.
Is this somehow possible at compile-time?

I tried already to use a consteval function with a value argument which is used to create the desired MyRange type, but unfortunately parameters of consteval functions are not constexpr, although the function is guaranteed to be executed at compile-time. All what I would need would be some way to implicitly take the integer literal from the formula (known at compile-time) to create the desired type that uses the value of the literal as template parameters.

2

There are 2 best solutions below

4
Davis Herring On

No: the type of an expression cannot depend on the value of a literal in it (outside of any template argument or array bound, and with the special exception for 0 possibly being a pointer). This is the “constexpr function parameters” matter all over again: maybe someday, but not now.

3
palapapa On

If you don't mind typing two extra characters, you can use a numeric literal operator template:

#include <iostream>
#include <cctype>
#include <algorithm>

template< int LOWER_, int UPPER_ >
class MyRange final {
public:
    constexpr static int LOWER = LOWER_;
    constexpr static int UPPER = UPPER_;

    template< int, int >
    friend class MyRange;

    constexpr MyRange(MyRange const &) noexcept = default;
    constexpr MyRange(MyRange&&) noexcept = default;
    constexpr ~MyRange() {}

    template< int VALUE >
    requires ( VALUE >= LOWER && VALUE <= UPPER )
    static constexpr
    MyRange wrap = MyRange( VALUE );

    template< class _RHS, int _RHS_LOWER = _RHS::LOWER, int _RHS_UPPER = _RHS::UPPER,
        int _RES_LOWER = LOWER + _RHS_LOWER, int _RES_UPPER = UPPER + _RHS_UPPER,
        typename _RESULT_T = MyRange<_RES_LOWER, _RES_UPPER> >
    friend
    _RESULT_T
    constexpr operator+(MyRange const lhs, _RHS const &rhs) noexcept {
        int result = lhs.value + rhs.unwrap();
        return construct<_RESULT_T>( result );
    }

    int unwrap() const noexcept { return value; }

private:
    MyRange() = delete;
    MyRange& operator=(MyRange const &) = delete;
    MyRange& operator=(MyRange&&) = delete;

    // this must not be exposed because value has to be checked against limits at compile-time;
    // wrap<VALUE> is the public "constructor"
    explicit constexpr MyRange(int value) noexcept : value(value) {}

    // helper: construct another specialization of MyRange
    template< class TO >
    static constexpr TO construct(int value) noexcept { return TO(value); }

    int const value;
};

constexpr int charDigitToInt(char c, std::size_t digit)
{
    int result = c - '0';
    for (size_t i = 0; i < digit; i++)
    {
        result *= 10;
    }
    return result;
}

template <std::size_t size, std::size_t digit>
constexpr int charArrayToInt(const char chars[size])
{
    if constexpr (digit == (std::size_t)-1)
    {
        return 0;
    }
    else
    {
        return charDigitToInt(chars[size - digit - 1], digit) + charArrayToInt<size, digit - 1>(chars);
    }
}

template <char ...Chars>
constexpr auto operator ""_r()
{
    constexpr std::size_t length = sizeof...(Chars);
    constexpr char chars[length]{ Chars... };
    static_assert(std::all_of(chars, chars + length, [](char c) { return isdigit(c); }), "The argument to _r must be a positive integer");
    constexpr int value = charArrayToInt<length, length - 1>(chars);
    return MyRange<value, value>::template wrap<value>;
}

int main()
{
    auto value = MyRange<5,20>::wrap<8>;
    auto result = value + 6_r;
    // 14; limits: 11, 26
    std::cout << result.unwrap() << "; limits: " << decltype(result)::LOWER << ", " << decltype(result)::UPPER << std::endl;
}

Here, digit means the exponent of a digit, so for the units place, digit is 0, for the tens place, digit is 1, and so on.

However, a numeric literal operator template doesn't work with negative numbers, so to support it, you need to add an overload for the unary minus operator to MyRange.

Edit: It's probably possible to do this without making ""_r a template by using recursion.