1

I've got a recursive variadic template function that I can use to evaluate its arguments at compile time, to make sure none of them are larger than a specified maximum value:

 #include <type_traits>

 // base-case
 template <int MaxVal, typename T>
 constexpr T returnZeroIffValuesAreNotTooBig(const T t) 
 {
    return (t>MaxVal)?1:0;
 }

 // recursive-case
 template <int MaxVal, typename T, typename... Rest> constexpr T returnZeroIffValuesAreNotTooBig(const T t, Rest&&... rest)
 {
    return returnZeroIffValuesAreNotTooBig<MaxVal>(t) 
         + returnZeroIffValuesAreNotTooBig<MaxVal>(std::forward<Rest>(rest)...);
 }

 int main(int argc, char ** argv)
 {
    static_assert(returnZeroIffValuesAreNotTooBig<6>(1,2,3)==0, "compiles (as expected)");
    static_assert(returnZeroIffValuesAreNotTooBig<6>(1,2,3,4,5,6,7)==0, "generates compile-time error (as expected, because one of the args is greater than 6)");
    return 0;
 }

... so far, so good, all of the above works fine for me.

Now I want to use that function in my class's variadic constructor:

 template<int MaxVal> class MyClass
 {
 public:
    template<typename ...Vals> constexpr explicit MyClass(Vals&&... vals)
    {
       // verify at compile-time that all values in (vals) are <= MaxVal
       static_assert(returnZeroIffValuesAreNotTooBig<MaxVal>(std::forward<Vals>(vals)...)==0, "why doesn't this compile?");
    }
 };

 int main(int argc, char ** argv)
 {
    MyClass<5> b(1,2,3,4);  // should compile, but doesn't!?
    return 0;
 }

... this won't compile; instead I always get this error:

 $ g++ -std=c++11 ./test.cpp
 ./test.cpp:12:80: error: static_assert expression is not an integral constant
       expression
   ...returnZeroIffValuesAreNotTooBig<MaxVal>(std::forward<Vals>(vals)...)==0...
      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~
 ./test.cpp:21:15: note: in instantiation of function template specialization
       'MyClass<5>::MyClass<int, int, int, int>' requested here
    MyClass<5> b(1,2,3,4);
               ^
 1 error generated.

... apparently my perfect forwarding is imperfect, and that is preventing the returnZeroIffValuesAreNotToBig function from being evaluated at compile-time? Can someone give me a hint as to what I need to differently in order to get this to work?

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • Should your `returnZeroIffValuesAreNotTooBig` function not return an `int`? – Some programmer dude Aug 04 '18 at 05:56
  • If I'm getting this correctly, is that you can't use function parameters in the `static_assert` call: https://stackoverflow.com/questions/8626055/c11-static-assert-within-constexpr-function – Caramiriel Aug 04 '18 at 07:02
  • @Someprogrammerdude I can change it to return an int but that doesn't seem to solve the problem at hand (in practice typename T will always be int or similar in any case) – Jeremy Friesner Aug 04 '18 at 12:10

1 Answers1

2

The perfect-forwarding is not-guilty.

As pointed by Caramiriel, you can't use a function parameter in a static_assert() test.

I know that the constructor is constexpr, but a constexpr constructor (like any other constexpr function) can be executed compile-time or run-time, according to the circumstances.

So a static_assert() test with values potentially known only run-time is an error.

Moreover, in your particular case

MyClass<5> b(1,2,3,4); 

you don't define b as constexpr so the compiler, even if can choose to execute it compile-time, usually execute it run-time.

If you want execute a static_assert() you have to make the values available compile time. The way I know is pass the values as template values. Unfortunately there isn't a way to explicit a template value for a contructor, so I see two possible ways

1) pass the vals... as class template parameters.

Something as follows (caution: code not tested)

 template<int MaxVal, int ... vals> class MyClass
 {
 public:
    constexpr MyClass ()
    {
       // verify at compile-time that all values in (vals) are <= MaxVal
       static_assert(returnZeroIffValuesAreNotTooBig<MaxVal>(vals...)==0,
                     "why doesn't this compile?");
    }
 };

MyClass<5, 1, 2, 3, 4>  b;

In this case you can also place the static_assert() in the body of the class, not necessarily in the body of the constructor.

2) pass the vals... as template parameter in a std::integer_sequence (available only starting from C++14, but is trivial substitute it with a custom class in C++11) constructor argument.

Something as follows (caution: code not tested)

 template<int MaxVal> class MyClass
 {
 public:
    template <int ... vals>
    constexpr MyClass (std::integer_sequence<int, vals...> const &)
    {
       // verify at compile-time that all values in (vals) are <= MaxVal
       static_assert(returnZeroIffValuesAreNotTooBig<MaxVal>(vals...)==0,
                     "why doesn't this compile?");
    }
 }; 

MyClass<5> b(std::integer_sequence<int, 1, 2, 3, 4>{});
max66
  • 65,235
  • 10
  • 71
  • 111
  • Hmm... (2) works, but requires C++14 (and also a bit more typing at the declaration site of the object in main(); in my real software there are many such declarations so I'd like to minimize their verbosity). (1), if I understand it correctly, would make the arguments part of the MyClass type, making it awkward to e.g. assign a MyClass with one set of arguments to another one. Maybe I will just have to settle for run-time checking only :( – Jeremy Friesner Aug 04 '18 at 12:29
  • 1
    You're right: with (1) you have different types for different `vals...`; for this reason I prefer solution (2); the C++14 problem can be smply solved with a custom struct; example: `template struct foo {}; MyClass<5> b(foo<1, 2, 3, 4>);` and I don't think it's too much verbose; you can also create a `make_MyClass()` function: `template constexpr MyClass make_MyClass () { return { std::integer_sequence{} }; } /*...*/ auto b = make_MyClass<5, 1, 2, 3, 4>();` – max66 Aug 04 '18 at 12:42