AlexKven
AlexKven

Reputation: 138

Why isn't this constexpr in a variadic function template constant?

In my class (which is a variadic class template), I need a constexpr for the sizeof() of the largest type passed in a variadic template. Like this:

template<class... Types>
class DiscriminatedUnion
{
.
.
.
static constexpr auto value = maxSizeOf<Types...>();

The code I have come up with for maxSizeOf() is the following:

template <class T>
static constexpr T static_max(T a, T b) {
    return a < b ? b : a;
}

template <class T, class... Ts>
static constexpr T static_max(T a, Ts... bs) {
    return static_max(a, static_max(bs...));
}

template <class T>
static constexpr int maxSizeOf() {
    return sizeof(T);
};

template <class T, class... Ts>
static constexpr int maxSizeOf() {
    return static_max(sizeof(T), maxSizeOf<Ts...>());
};

But in Visual Studio 2017, I am getting a compile error "expression did not evaluate to a constant."

I am not sure what it is that is not allowing the expression to be constant. I've tried compiling different things to ensure that they can be constant. I've tried using a sizeof() with a template parameter in a constexpr function, which works, which I expect since the size of types is always known at compile time. Integer arithmetic and comparison seems to be valid in a constexpr function, but I tried some again for verification. Then I've tried using integer arithmetic in the variadic template method, without sizeof(), with the following:

template <class T>
static constexpr int maxSizeOf(int n) {
    return n;
};

template <class T, class... Ts>
static constexpr int maxSizeOf(int n) {
    return static_max(n, maxSizeOf<Ts...>(n + 1));
};

static constexpr int numBytes = maxSizeOf<Types...>(1);

And this does not work. So I'm thinking it must be something to do with the variadic method template expansion. But this should be able to be made a compile-time constant, because variadic template packs are always expanded at compile time. Does anyone know why these can't be constexpr?

Upvotes: 1

Views: 1084

Answers (2)

max66
max66

Reputation: 66200

The problem of your code is that when you call max_sizeof<T>() with a single T types, both

template <class T>
static constexpr int maxSizeOf() {
    return sizeof(T);
};

and

template <class T, class... Ts>
static constexpr int maxSizeOf() {
    return static_max(sizeof(T), maxSizeOf<Ts...>());
};

matches. So the compiler can't choose the correct one.

You can solve with if constexpr ( sizeof...(Ts) ), as suggested by dontpanic, but if constexpr is available only starting from C++17.

A possible (and elegant, IMHO) solution, working also in C++11 and C++14, is delete the only-one-type function and add the following zero-type function

template <int = 0>
static constexpr std::size_t maxSizeOf()
 { return 0u; };

This way when you call maxSizeOf<Ts...>(), when sizeof...(Ts) > 0u, the one-or-more-type version is called; when sizeof...(Ts) == 0u (that is: when the Ts... list is empy), the int = 0 (no types) matches.

Onother suggestion: sizeof() is a std::size_t value so is better if maxSizeOf() return a std::size_t

The following is a full working (also C++11) solution

#include <iostream>

template <typename T>
static constexpr T static_max (T a, T b)
 { return a < b ? b : a; }

template <typename T, typename ... Ts>
static constexpr T static_max (T a, Ts ... bs)
 { return static_max(a, static_max(bs...)); }

template <int = 0>
static constexpr std::size_t maxSizeOf()
 { return 0u; };

template <typename T, typename ... Ts>
static constexpr std::size_t maxSizeOf()
 { return static_max(sizeof(T), maxSizeOf<Ts...>()); };

template <typename ... Ts>
struct foo
 { static constexpr auto value = maxSizeOf<Ts...>(); };

int main ()
 {
   std::cout << foo<int, long, long long>::value << std::endl;
 }

But, as observed by aschepler (thanks!), this solution works but doesn't use at all the variadic version of static_max().

An alternative, that uses the variadic version of static_max(), is rewrite the variadic version of maxSizeOf() not in a recursive way but simply unpacking the variadic list as follows

template <typename ... Ts>
static constexpr std::size_t maxSizeOf()
 { return static_max(sizeof(Ts)...); } 

Now is the ground case (the zero-type version) of maxSizeOf() that isn't used anymore and can be deleted.

Anyway, as suggested by NathanOliver, you can use std::max() (the version receiving an initializer list) that, starting from C++14, is constexpr.

So, starting from C++14, you can simply write

#include <algorithm>
#include <iostream>

template <typename ... Ts>
struct foo
 { static constexpr auto value = std::max({sizeof(Ts)...}); };

int main ()
 {
   std::cout << foo<int, long, long long>::value << std::endl;
 }

Upvotes: 2

dontpanic
dontpanic

Reputation: 351

"expression did not evaluate to a constant." seems not the root cause. Your static_max and maxSizeOf need modifications to make the compiler happy. You can refer to this post to see how to do that under different C++ standards.

For example :

template <class T, class... Ts>
static constexpr T static_max(T a, Ts... bs) {
    if constexpr (sizeof...(Ts) == 0)
        return a;
    else
        return std::max(a, static_max(bs...));
}

template <class T, class... Ts>
static constexpr int maxSizeOf(int n) {
    if constexpr (sizeof...(Ts) == 0)
        return n;
    else
        return static_max(n, maxSizeOf<Ts...>(n + 1));
};

Actually, we don't need static_max at all. All we need here is to find the max within just 2 values, and std::max is already there.

EDIT: it seems that we don't need maxSizeOf either... as Nathan mentioned in the comments, std::max can deal with initializer_list as well.

Upvotes: 1

Related Questions