the_summer
the_summer

Reputation: 469

Enforce compile time check during initialization of non-integral types

I am trying to make sure that variables are initialized with valid data and would like to have this check carried out at compile time. The classes have a static constexpr method to check validity. In a simplified version it could be like this:

template <typename T>
struct Foo {
    constexpr Foo(T initValue) :
      mValue(initValue) 
    {
         assert(isValid(iniValue));
    }

    static constexpr bool isValid(T value) {
        return (value > 0);
    }        
private:
    T mValue;
};

Now for integral types I could use a templated function to enforce compile time checks:

template <typename T, T initVal>
Foo<T> make_new() {
    static_assert(Foo<T>::isValid(initVal), "is not valid");
    return  Foo<T>(initVal);
}

And use it like this

    auto saveInt = make_new<int, 3>();

The question is how to do that with non-integral types. I could do something like this which would trigger the assert in C++14:

    constexpr Foo<float> constFloat(3.0);
    Foo<float> saveFloat = constFloat;

Is there a maybe a more elegant way in C++>=11 which would allow me to do that with a one-liner? In the end the value I want to assign is known at compile time and so is the validity of this value.

Upvotes: 1

Views: 218

Answers (2)

max66
max66

Reputation: 66200

What about a constexpr check-and-return filter, with a not consexpr instruction (the throw of an exception) in it's wrong case, as follows

static constexpr T checkAndRet (T value)
 { return value > 0 ? value
                    : (throw std::runtime_error("invalid"), T{}); } 

that you can apply to the value before the assignment?

constexpr Foo(T initValue) : mValue{checkAndRet(initValue)} 
 { }

So you can have a compile time error when you declare a constexpr value

constexpr Foo<float> f2 {-1.0f};  // compile time error

and a runtime error (an exception) otherwise

Foo<float> f3 {-1.0f};  // run time error

The following is a full working example and work starting from C++11

#include <stdexcept>

template <typename T>
struct Foo {
    constexpr Foo(T initValue) : mValue{checkAndRet(initValue)} 
     { }

    static constexpr T checkAndRet (T value)
     { return value > 0 ? value
                        : (throw std::runtime_error("invalid"), T{}); }

private:
    T mValue;
};

int main ()
 {
   Foo<float> f0 {1.0f};               // OK
   constexpr Foo<float> f1 {1.0f};     // OK
   //constexpr Foo<float> f2 {-1.0f};  // compile time error
   Foo<float> f3 {-1.0f};              // run time error
 }

-- EDIT --

The OP specify the following

It does not solve my initial problem. Your last code line still produces an error only at runtime, i.e. initializing a Foo<float> variable to a value validated during compile time is not possible unless I declare the init value as constexpr beforehand

Sorry: I partly misunderstood your question.

Yes; it's possible, without declaring a constexpr Foo<float> beforehand, but the solution that come in my mind is very ugly because it's long and require the repetitions of elements.

With a C-style macro you can make it simple and elegant but I think C-style macros are distilled evil.

Anyway... if you declare the following static constexpr methods in Foo

static constexpr bool isValid (T value)
 { return value > 0 ? true
                    : (throw std::runtime_error("invalid"), false); }        

template <bool>
static constexpr T retV (T value)
 { return value; }

you can initialize a not-constexpr Foo<float> object with a float literal and a compile time check as follows

// OK
Foo<float> f0 { Foo<float>::retV<Foo<float>::isValid(1.0f)>(1.0f) };

// compilation error
// Foo<float> f1 { Foo<float>::retV<Foo<float>::isValid(-1.0f)>(-1.0f) };

As you can see is very ugly and you have to repeat two times the value, that is very error prone.

But if you define a C-style macro as follows

#define makeCValue(val) \
   Foo<decltype(val)>::retV<Foo<decltype(val)>::isValid(val)>(val)

You can initialize you Foo<float>() objects as follows

Foo<float> f2 { makeCValue(1.0f) };     // OK
// Foo<float> f3 { makeCValue(-1.0f) }; // compilation error

The following is a full working example

#include <stdexcept>

template <typename T>
struct Foo
 {
   constexpr Foo(T initValue) : mValue{initValue} 
    { }

   static constexpr bool isValid (T value)
    { return value > 0 ? true
                       : (throw std::runtime_error("invalid"), false); }

   template <bool>
   static constexpr T retV (T value)
     { return value; }

   T mValue;
 };

#define makeCValue(val) \
   Foo<decltype(val)>::retV<Foo<decltype(val)>::isValid(val)>(val)


int main ()
 {
   // OK
   Foo<float> f0 { Foo<float>::retV<Foo<float>::isValid(1.0f)>(1.0f) };

   // compilation error
   // Foo<float> f1 { Foo<float>::retV<Foo<float>::isValid(-1.0f)>(-1.0f) };

   Foo<float> f2 { makeCValue(1.0f) };     // OK
   // Foo<float> f3 { makeCValue(-1.0f) }; // compilation error
 }

Obviously you can use makeCValue() macro only with compile time known values (literal values, constexpr values, ...).

Upvotes: 1

bolov
bolov

Reputation: 75697

With the current standard no, you can't do it at compile time.

At Albuquerque ISO C++ Committee Meeting in 2017 there was positive feedback on Proposals for user-defined literals for strings, class/struct types as non-type template parameters, new and delete in constexpr contexts. So in a future version of the standard (if we are luck maybe C++20) it will be possible. Until then the current restrictions only allows integral types for non-type template parameters and there is no workaround I am aware of.

As for the reason of the current limitations is mostly has to do with representation, name mangling and comparisons of values that are of a non-integer type.

Upvotes: 1

Related Questions