8

Say I wanted to write a generic class that maintains an integer that always stays between two values. Something like this:

template<int Lower, int Upper>
class MyInt {
    private:
        int m_int;
    public:
        // Constructors, operators...
};

The class invariant is that Lower <= m_int <= Upper. Of course MyInt should have all the usual operations that integers tend to have, like assignment and arithmetic operators. MyInt would throw if an operation were to leave it in a state that breaks its invariant. In many cases, however, this should be compile-time detectable. Consider this example code:

int foo = 500;
constexpr int const bar = 500;
MyInt<0,100> a = 15; // OK
MyInt<0,100> b = foo; // Throws at runtime
MyInt<0,100> c = 500; // Compile error?
MyInt<0,100> d = bar; // Compile error?
MyInt<0,100> f = std::integral_constant<int, 500>; // Compile error

For std::integral_constant, writing the appropriate constructor is straight-forward. But is it possible to compile-time-detect that a is within range and c and d aren't? The assigned values are either literals known at compile time or constexpr constants.

I have tried SFINAE-ing around and whatnot, but I could not find a way to go from value-semantics to template arguments, even for these cases where (I claim) the values are clearly compile-time constants.

Is it true for C++17 that the language does not provide the tools required to implement this? If yes, is this going to change with the upcoming C++20? In case what I'm looking for is impossible, what are the reasons for this? My interest is entirely educational, so I would be interested if I'm missing something (like literals not actually being compile-time constants or something).

Note: I am aware that case f can be made less ugly by introducing a custom literal suffix and requiring the user to type something like this:

MyInt<0,100> g = 15_constint; // OK
MyInt<0,100> h = 500_constint; // Compile error

I am also aware of the possibility to provide an interface like this:

MyInt<0,100> i;
i.assign<500>(); // Compile error

However both these workarounds come with a certain typing overhead and are not necessarily idiomatic C++.

Drag-On
  • 362
  • 2
  • 9
  • 1
    [constexpr](https://en.cppreference.com/w/cpp/language/constexpr) and/or [static_assert](https://en.cppreference.com/w/cpp/language/static_assert) ? – Jesper Juhl Jun 03 '18 at 14:38
  • *"... if I'm missing something (like literals not actually being compile-time constants or something)"* - It is amazing how brain dead the language is at times. It is as if the C++ committee pretends a source file will somehow change once it has been saved to disk, which disobeys all the laws of physics as we understand them... – jww Jun 03 '18 at 14:41
  • @jww well, a compilation unit could become many different things at compile time even when based on the same source file, depending on the values of preprocessor macros :-/ – Jesper Juhl Jun 03 '18 at 14:44
  • "*is this going to change with the upcoming C++20?*" C++20 isn't done, so asking about what will or won't be in it is kind of speculative. – Nicol Bolas Jun 03 '18 at 14:46

2 Answers2

9

In case what I'm looking for is impossible, what are the reasons for this?

Basically it comes down to the fact that the C++ function model does not permit a function to recognize a distinction between a constexpr parameter and a runtime parameter. If a function takes an int, then it takes an int. That function's behavior cannot be different based on whether that int is supplied by a constant expression (like a literal) or a runtime value.

And this is broadly true even for constexpr functions. You can't have a constexpr function which can do compile-time checks on its parameters. The evaluation can be done at compile-time, but doing foo(500); must ultimately behave the same as doing auto b = 500; foo(b);.

This is why the workarounds all involve using tricks to put the constant expression in a template argument, where such recognition is possible. Of course, a template argument can never be a runtime value, so that creates other issues (like having to have multiple functions). And being a template argument is a generally painful thing to work with.

Essentially, what you want requires that a function can declare that a parameter is constexpr (along with overloading rules to allow you to have a non-constexpr version). No such proposal has been voted into C++20. The best that's been done is P1045, which hasn't even been discussed by the committee yet, and has a bunch of holes that need to be filled in. So I wouldn't hold my breath.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 2
    I disagree that `constexpr ` function can't have compile time checks. Take a look at my answer and more generally ah meaning of throw in constexpr functions – bartop Jun 03 '18 at 15:06
  • @bartop: I said "this is ***broadly*** true". – Nicol Bolas Jun 03 '18 at 15:07
  • [if constexpr](http://en.cppreference.com/w/cpp/language/if#Constexpr_If) would be a "compile time check" I'd say. – Jesper Juhl Jun 03 '18 at 15:12
  • 1
    @JesperJuhl: One which cannot be used on *parameters*, because function parameters are never constant expressions. – Nicol Bolas Jun 03 '18 at 15:43
7

Try this one. It's example that is quite simply scalable:

template<int Max, int Min>
class MyInt
{
public:
    constexpr MyInt(int i)
        : m_int(i > Max
            ? throw std::exception("out of bounds int")
            : ( i < Min
                ? throw std::exception("out of bounds int")
                : i))
    {}

private:
    int m_int;
};


int main()
{
    MyInt<1, 2> i(4);
}

It shows compile time error :). I used this SO question as guideline: constexpr constructor with compile time validation

Alexis Wilke
  • 19,179
  • 10
  • 84
  • 156
bartop
  • 9,971
  • 1
  • 23
  • 54
  • 1
    This is a neat trick, but it only works if you make your variables constexpr, i.e. `constexpr MyInt<1, 2> i (4);` But what I'm looking for is something where i does not have to be constexpr; after all, 4 is known at compile time. – Drag-On Jun 03 '18 at 15:48
  • @Drag-On: In order to do compile-time testing, you need compile-time *evaluation*. In this case, the evaluation of the initialization of `i`. That's spelled `constexpr` in C++. Non-`constexpr` variables may get compile-time initialization, but that's up to the compiler. It is only *guaranteed* for `constexpr` variables. – Nicol Bolas Jun 03 '18 at 15:59
  • @Drag-On You can always make constexpr factory friend function of this class and make constructor private. This will enforce the check while calling the method. But of course necessity to make a call instead of assignment can be annoying. Another dirty solution is preprocessor usage – bartop Jun 04 '18 at 14:22
  • Note that for a bound error you could use the `std::range_error` instead of the default `std::exception`. – Alexis Wilke Jul 03 '21 at 00:31