9

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.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
WangChu
  • 279
  • 1
  • 8
  • 6
    [CWG Issue #2118](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#2118) will make this program ill-formed in the future. See [this question](https://stackoverflow.com/questions/44267673/is-stateful-metaprogramming-ill-formed-yet) for more details. – Rakete1111 Jul 12 '17 at 10:12
  • @Rakete1111 Okay, I get it. But I still have the question, why can't I omit the variable "value"? – WangChu Jul 12 '17 at 10:48

2 Answers2

3

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.

Ap31
  • 3,244
  • 1
  • 18
  • 25
  • Is this a bug or a feature? – WangChu Jul 12 '17 at 11:05
  • Do you mean that when the 'b','c','d' are initializing, they just run the same code? – WangChu Jul 12 '17 at 11:19
  • I do believe that this is a valid behavior. Yeah, I think `b`, `c` and `d` are initialized with the same code. I'm trying to expand my answer now – Ap31 Jul 12 '17 at 11:23
  • Irrelevant. This is `constexpr`, inlining only applies to runtime behavior. – MSalters Jul 12 '17 at 11:23
  • @MSalters I agree, inlining is a very unfortunate term, I'll remove or edit – Ap31 Jul 12 '17 at 11:31
  • @Ap31 Can this be considered a compiler bug? The code is well-formed, as far as I understand it. So, a standards compliant compiler cannot perform optimizations that will change the behavior of the program. You say that the only reason of that behavior is caching, but in that case, the compiler is not allowed to cache, because without caching, the code works as expected. – Rakete1111 Jul 12 '17 at 12:21
  • 2
    I think the formal term is ["memoizing"](https://en.wikipedia.org/wiki/Memoization), not caching. But feel free to blame me for "caching". – MSalters Jul 12 '17 at 12:28
  • @MSalters This answer says that the `reader` function that I rewrite fails because that the compiler caches the call. But in your answer, you said that my function fails because of "substitution failure"(`reader` is not instantiated). So, which is the exact reason in this situation? – WangChu Jul 12 '17 at 12:49
  • @WangChu Perhaps it can be considered a compiler bug in a sense that compiler does not follow the exact wording of standard. But it is a standard that needs to be fixed here, not a compiler – Ap31 Jul 12 '17 at 13:44
3

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 ;)

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • 2
    Sorry, I still don't understand it very well. Do you mean that the var "value" instantiates "Flag"(the struct) rather than "flag"(the function)? I think the call "Next" instantiate "flag"(the function). And in the "reader" function that I rewrite, wouldn't the line "return reader(0, Flag{});" instantiates "Flag"? – WangChu Jul 12 '17 at 11:46
  • @WangChu: Oops, my bad. (Although using both `flag` and `Flag` is not the cleanest approach IMHO). Calling `next()` without arguments requires the use of its default arguments. That argument relies on `reader(0, Flag<0>{})` which is an overloaded function call expression. It's that overload resolution which triggers SFINAE. – MSalters Jul 12 '17 at 11:59
  • Sorry, I just still don't understand... Whether I rewrite the `reader` function or not, the `Next` function uses `reader(0, Flag<0>{})` to trigger SFINAE. But if I use `int value` as a parameter, it triggers recursion; If i omit `int value`, it doesn't trigger recursion, why? – WangChu Jul 12 '17 at 12:25
  • 2
    @WangChu: Look at the default argument - `int value = reader(0, Flag{})`. Since you don't provide an actual value (value is the 3rd parameter, but you call `reader` with 2 arguments), this overload has a viable number of arguments **if** the default value can be instantiated. But this is rather circular logic - the default value for `reader` uses `reader` if that's already instantiated, or the `float` version otherwise. – MSalters Jul 12 '17 at 12:26
  • Why isn't the template instantiated in the body of the function? Because as far as I understand, the only thing stopping the recursion is `Writer`, who defines the friend function to make the second overload valid. – Rakete1111 Jul 12 '17 at 12:26
  • @Rakete1111: Exactly what do you mean by "instantiated in the body of the function"? One of the main reasons this is a CWG issue is because the observed behavior depends on the exact implementation of the template instantiation mechanism. By mandating a specific behavior (instead of plain forbidding it). WG21 would constrain implementations. – MSalters Jul 12 '17 at 12:33
  • how can `value` participate in SFINAE? `reader(0, Flag{})` is always successfully resolved to something, whether `reader(int, ...)` or `reader(float,...)`. I believe it's only the `int = flag(Flag{})` part that causes SFINAE. – Ap31 Jul 12 '17 at 12:50
  • 2
    @Ap31: Ow. I might have misread the code there (which goes to show the fundamental problem with this code, I have to retrace the steps of the compiler). That said, the fact that a particular call is resolved to _something_ is exactly the point of SFINAE. The `reader(float, ...)` is usually not selected in overload resolution, as it involves a conversion from 0 (`int`) to 0.0f (`float`). When SFINAE silently excludes `reader(int, ...)`, the `float` overload is the sole remaining option. – MSalters Jul 12 '17 at 12:55
  • @MSalters I agree on both your points (SFINAE and the pain of reading this code). I just don't see what `value` has to do with anything - overloads or SFINAE. Personally I feel like my answer explains it better, but I can't be 100% sure since I can't come up with any solid way to prove it :D – Ap31 Jul 12 '17 at 13:03
  • @Ap31: We might be answering different questions. As I read it, the question is "why is my version behaving differently?". I'll update my answer to reflect this. – MSalters Jul 12 '17 at 13:06
  • @MSalters Would you say that if we combine the original declaration (`reader(int, Flag, int value = reader(0, Flag{}))`) with the modified definition (`return reader(0, Flag{});`) the resulting output will depend on wheter your answer or mine is correct? :) – Ap31 Jul 12 '17 at 13:52
  • @MSalters Ah, I would that it was a CWG issue because the Standard explicitly allowed it. You say that the trick works because `Flag` is instantiated when the second `reader` is called, but this happens whether `value` is in the body or parameter list, so I don't see how it helps. I would have thought that it was `flag` that did the SFINAE, and not `value`. – Rakete1111 Jul 12 '17 at 16:09
  • @MSalters Hi, me again. I have tested some code about what you replied me in the comment above. This line "the default value for `reader` uses `reader` if that's already instantiated, or the `float` version otherwise", it doesn't work like this. It will just instantiate `reader` rather than use the `float` version. So I think what cause the difference is something about caching. Anyhow, thanks for your answer, it do helps me. And if you found that I still get something wrong, please notify me. @Ap31 – WangChu Jul 12 '17 at 16:56