Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does removing the default parameter break this constexpr counter?

Consider the following code that implements a compile time counter.

#include <iostream>

template<int>
struct Flag { friend constexpr int flag(Flag); };

template<int N>
struct Writer
{
    friend constexpr int flag(Flag<N>) { return 0; }
};

template<int N>
constexpr int reader(float, Flag<N>) { return N; }

template<int N, int = flag(Flag<N>{})>
constexpr int reader(int, Flag<N>, int value = reader(0, Flag<N + 1>{}))
{
    return value;
}

template<int N = reader(0, Flag<0>{}), int = sizeof(Writer<N>) >
constexpr int next() { return N; }


int main() {
    constexpr int a = next();
    constexpr int b = next();
    constexpr int c = next();
    constexpr int d = next();
    std::cout << a << b << c << d << '\n'; // 0123
}

For the second reader overload, if I put the default parameter inside the body of the function, like so:

template<int N, int = flag(Flag<N>{})>
constexpr int reader(int, Flag<N>)
{
    return reader(0, Flag<N + 1>{});
}

Then the output will become:

0111

Why does this happen? What makes the second version not work anymore?

If it matters, I'm using Visual Studio 2015.2.

like image 574
WangChu Avatar asked Jul 12 '17 10:07

WangChu


2 Answers

Without value being passed as a parameter, nothing stops the compiler from caching the call to reader(0, Flag<1>).

In both cases first next() call will work as expected since it will immediately result in SFINAEing to reader(float, Flag<0>).

The second next() will evaluate reader<0,0>(int, ...), which depends on reader<1>(float, ...) that can be cached if it does not depend on a value parameter.

Unfortunately (and ironically) the best source I found that confirms that constexpr calls can be cached is @MSalters comment to this question.

To check if your particular compiler caches/memoizes, consider calling

constexpr int next_c() { return next(); }

instead of next(). In my case (VS2017) the output turns into 0000.

next() is protected from caching by the fact that its default template arguments actually change with every instantiation, so it's a new separate function every time. next_c() is not a template at all, so it can be cached, and so is reader<1>(float, ...).

I do believe that this is not a bug and compiler can legitimately expect constexprs in compile-time context to be pure functions.

Instead it is this code that should be considered ill-formed - and it soon will be, as others noted.

like image 195
Ap31 Avatar answered Sep 21 '22 18:09

Ap31


The relevance of value is that it participates in overload resolution. Under SFINAE rules, template instantiation errors silently exclude candidates from overload resolution. But it does instantiate Flag<N+1>, which causes the overload resolution to become viable the next time (!). So in effect you're counting successful instantiations.

Why does your version behave differently? You still reference Flag<N+1>, but in the implementation of the function. This is important. With function templates, the declaration must be considered for SFINAE, but only the chosen overload is then instantiated. Your declaration is just template<int N, int = flag(Flag<N>{})> constexpr int reader(int, Flag<N>); and does not depend on Flag<N+1>.

As noted in the comments, don't count on this counter ;)

like image 43
MSalters Avatar answered Sep 24 '22 18:09

MSalters