Reputation: 469
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
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 asconstexpr
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
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