Search code examples
c++11variadic-templatesconstexprperfect-forwardingstatic-assert

Why does my variadic template argument verifier refuse to evaluate at compile time?


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?


Solution

  • 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>{});