Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

decltype with function template which has default argument make the confused result(a funny problem or gcc's bug)

In order to show the problem intuitively, you can look directly at the 'UPDATE' section

#include <iostream>
template<int N>
struct state
{
    static constexpr int value = N;
    friend auto create(state<N>);
};

template<int N>
struct generate_state
{
    friend auto create(state<N>) {
        return state<N>{};
    }
    static constexpr int value = N;
};

template struct generate_state<1>;

template<int N, typename  U = decltype(create(state<N - 1>{})) >
std::size_t getvalue(float,state<N>,int res = generate_state<N>::value) {  #1
    return N;
}

template<int N, typename U = decltype(create(state<N>{})) >
std::size_t getvalue(int, state<N>, int r = getvalue(0, state<N + 1>{})) { #2
    return N;
}
int main(){
   getvalue(0, state<1>{});
   using type = decltype(create(state<2>{}));
}

Consider the above code,the result is logical.Beause every time invoke the getvalue function will add the state once ,It's the stateful metaprogramming.
But ,if change the getvalue(0, state<1>{}); to using t = decltype(getvalue(0, state<1>{}));,the reuslt will be quite confused.

int main(){
  using t = decltype(getvalue(0, state<1>{})); #3
  using type = decltype(create(state<3>{}));
}

the above code can be complied in g++,It means the state added twice,this result is quite confused.In order to explain why there is such a result.The following are my guess:

at #3,to decide which getvalue to be used at the default arugment r,Both #1 and #2 are considered,before instantiting #1 ,generate_state<2> should be instantited firstly ,so state<2> was added, after that, no falis when #2 was substituted ,so #2 is the best match for state<2> and then state<3> was added.This process does not conform to the overloading rule of the function(in the case of normal,#1 and #2 only chose the one,the other is removed from the overload set). but it's not possible unless it's like this.why?

In order to show the complier process,add the static_assert to make the complier print some logs

main.cpp: In instantiation of ‘std::size_t getvalue(float, state<N>, int) [with int N = 2; U = state<1>; std::size_t = long unsigned int]’:
main.cpp:27:53:   required from here
main.cpp:22:2: error: static assertion failed: #1
  static_assert(!N, "#1");
  ^~~~~~~~~~~~~
main.cpp: In instantiation of ‘std::size_t getvalue(float, state<N>, int) [with int N = 3; U = state<2>; std::size_t = long unsigned int]’:
main.cpp:27:53:   required from here
main.cpp:22:2: error: static assertion failed: #1
main.cpp: In instantiation of ‘std::size_t getvalue(int, state<N>, int) [with int N = 2; U = state<2>; std::size_t = long unsigned int]’:
main.cpp:27:53:   required from here
main.cpp:28:2: error: static assertion failed: #2
  static_assert(!N, "#2");

In order to simplify the problem,Decompose the code as following:

template<int N, typename  U = decltype(create(state<N - 1>{})) >
std::size_t getvalue(float, state<N>, int res = generate_state<N>::value) {
    static_assert(!N, "#1");
    return N;
}

template<int N, typename U = decltype(create(state<N>{})) >
std::size_t getvalue(int, state<N>, int r = 0) {
    static_assert(!N, "#2");
    return N;
}

template<int N, typename U = state<N> >
std::size_t funproblem(int, state<N>, int r = getvalue(0, state<N + 1>{})) {
        return N;
}
int main() {
    using t = decltype(funproblem(0, state<1>{}));
}
main.cpp: In instantiation of ‘std::size_t getvalue(float, state<N>, int) [with int N = 2; U = state<1>; std::size_t = long unsigned int]’:
main.cpp:33:55:   required from here
main.cpp:22:2: error: static assertion failed: #1
  static_assert(!N, "#1");
  ^~~~~~~~~~~~~
main.cpp: In instantiation of ‘std::size_t getvalue(int, state<N>, int) [with int N = 2; U = state<2>; std::size_t = long unsigned int]’:
main.cpp:33:55:   required from here
main.cpp:28:2: error: static assertion failed: #2
  static_assert(!N, "#2"); 

both function template getvalue are instantited,what's the hell?In case of normal,decltype(create(state<N>{})) with N=2 will be substituted failed and will be removed from overload set,only the function template with the template parament U of decltype(create(state<N - 1>{})) with N=2 will be substituted successfully and to be instantited by the complier...

the quotes about function template with default arguments in standard document:

If a function template f is called in a way that requires a default argument to be used, the dependent names are looked up, the semantics constraints are checked, and the instantiation of any template used in the default argument is done as if the default argument had been an initializer used in a function template specialization with the same scope, the same template parameters and the same access as that of the function template f used at that point, except that the scope in which a closure type is declared ([expr.prim.lambda.closure]) – and therefore its associated namespaces – remain as determined from the context of the definition for the default argument. This analysis is called default argument instantiation. The instantiated default argument is then used as the argument of f

UPDATE:

The problem can be further simplified:

template<int N>
struct state
{
    static constexpr int value = N;
    friend auto create(state<N>);
};

template<int N>
struct generate_state
{
    friend auto create(state<N>) {
        return state<N>{};
    }
    static constexpr int value = N;
};
template struct generate_state<1>;

template<int N, typename  U = decltype(create(state<N-1>{})) >  #11
void getvalue(float, state<N>, int res = generate_state<N>::value) {
}

template<int N, typename U = decltype(create(state<N>{})) >  #22
std::size_t getvalue(int, state<N>, int r = 0) {
    return N;
}
int main() {
  using t = decltype(getvalue(0, state<2>{}));
  std::cout << typeid(t).name() << std::endl;
}

The gcc complier will print t = std::size_t. It means the complier chose the #22, but at this point of decltype(getvalue(0, state<2>{})), the defination of create(state<2>{}) does not exsit at all, #22 does not substitute successfully, it should be removed from the overload set, accroding to the result that complier printed,a ctually it is not, how suprise it is!

If you change decltype(getvalue(0, state<2>{})); to getvalue(0, state<2>{}), #11 is the best match and to be instantited, this is conforming to logic, because create(state<2>{}) is not defined at this point, so #22 will be substituted failed, #11 is best matched.

What makes the result so confused? Does anyone know why? Is it a gcc bug or anything else?

like image 448
xmh0511 Avatar asked Jan 02 '20 03:01

xmh0511


2 Answers

Let us consider just the "update" section. You are relying on a very dangerous property - a state of the type system computation. Namely, create(state<2>) remains undefined, until a seemingly unrelated struct generate_state<2> is instantiated.

Any sane type system in any respectable language is (or should be) stateless. A given type expression is a constant throughout the whole compilation process. With it, the compiler can employ complex reasoning algorithms to match types and check the correctness of the program.

The mechanism you use defy this. Such approach can lead to really weird results. A perfect question: Is stateful metaprogramming ill-formed (yet)? shows what it can lead to:

static_assert(!std::is_same_v<S<>, S<>>, "This is ridiculous");

to be actually accepted by the compiler! (follow the link above to see full example, I don't want to copy-paste it here).

So in short: don't use it! If you want to be able to switch between different implementations using type system, use stateless aproach, as shown in the other answer of mine (which I leave for reference).

Different compilers seem to work in different ways when stateful type computation is encountered. You are at the mercy of their internals. The decltype scenario of yours show weird behavior of the g++ implementation. It seems that within the context of decltype it is actually able to instantiate auto create(state<N>) as if it was a stand-alone template.

This compiles with g++ 9.2:

int main() {
  using t = decltype(getvalue(0, state<2>{}));
  std::cout << typeid(t).name() << std::endl;
  auto result = getvalue(0, state<2>{});
  std::cout << typeid(decltype(result)).name() << std::endl;
}

https://godbolt.org/z/HdtKFd

The decltype(getvalue(0, state<2>{})) manages to instantiate create<2> and then the auto result = getvalue(0, state<2>{}) successfully compiles, using #22. However, if you comment out the first 2 lines, the 3-rd line suddenly switches to #11 and fails.

So, what the standard says about it? Not much. Probably because it is hard to specify precisely what should be considered ill-formed. Check out this answer for a bit more elaborate answer: https://stackoverflow.com/a/44268181/635654

like image 156
CygnusX1 Avatar answered Nov 20 '22 10:11

CygnusX1


Update:

Understanding the problem:

This is some interesting code! As you state in the comments to my original answer, the crux here is the friend auto declarations inside the state<N> and generate_state<N> classes.

If I understand your idea, the point is to declare the classes in such a way that create(state<x>) is only defined if generate_state<x> has also been declared in this scope.

Digging further into your code, I believe I have understood what is going on.

What is happening

To understand what is happening, let us take a look at your second example.

Let us change main to the following:

int main() {
    using t = decltype(getvalue(0, state<1>{})); // Line 1
    using u = decltype(getvalue(0, state<2>{})); // Line 2
    using v = decltype(getvalue(0, state<3>{})); // Line 3

    std::cout << typeid(t).name() << std::endl;
    std::cout << typeid(u).name() << std::endl;
    std::cout << typeid(v).name() << std::endl;
}

This also compiles and produces

std::size_t (actually it is just 'm' on my machine, but anyhow...)
std::size_t
std::size_t

What is happening here is the following:

On line 1, #11 will fail to resolve, since create(state<0>) does not exist, this is a substitution failure and is therefore not an error. #22 will resolve and is therefore used.

On line 2, #11 will resolve, and in resolving it will resolve generate_state<2>::value. This statement adds create(state<2>) to the compiler's symbol table.

Following this, line 2 will try to resolve #22. And intuitively we would expect this to fail. However, since #11 just resolved, create(state<2>) is now available, and #22 resolves as well. int is a better match than float so #22 is chosen.

The same thing now happens for line 3, since create<(state<2>) is available.

It is made even more clear if you again alter main to the following:

int main() {
    using t = decltype(getvalue(0, state<1>{})); 
    using v = decltype(getvalue(0, state<3>{})); // Line 2 and 3 are swapped.
    using u = decltype(getvalue(0, state<2>{})); 

    std::cout << typeid(t).name() << std::endl;
    std::cout << typeid(u).name() << std::endl;
    std::cout << typeid(v).name() << std::endl;
}

As doing so will cause the compiler to fail.

The compiler fails because on (the new) line 2, create(state<2>) is not yet available, so #11 fails to resolve. As #11 fails to resolve,create(state<3>)` is never added to the symbol table and thus #22 also fails to resolve, resulting in a compilation error.

Likewise, changing the default parameter in #11 to state<N>::value will cause the #11 to be picked over #22 for get_value(0, state<2>). If you do this, all states other than 1 and 2 will fail (as expected).


Original Answer: Kept to explain comments.

To my eye your examples behave as expected. You seem to have misunderstood parts of the fundamentals about template instantiations. I'll go through them in turn:

When you write:

It means the complier chose the #22 ,but at this point of decltype(getvalue(0, state<2>{})),the defination of create(state<2>{}) does not exsite at all

This statement is false. One of the characteristics of a template class/struct is that the type will be declared whenever it is needed.

This means that the statement:

template struct generate_state<1>;

Is not really doing anything in this example. You can safely remove it and the code will still work in exactly the same way. The only reason to use the statement above, is when you want a certain version of a template to be referenced in the given compilation unit (and thus type-substituted and written to code).

The other thing I think you have misunderstood is how the template functions are compiled.

As you already know, when writing a normal template function, there are two stages to its calling. First, during compilation, the template parameters are substituted and the function is written to code. Second, when the function is called, the previously written code is executed with the given arguments, normally this only happens at runtime, but when invoking the function is a constexpr context the function may be executed at compile-time.

This is the core of metaprogramming: To design logic which is executed at compile-time. The output from the metaprogramming execution is the code that will execute.

So the reason your static_asserts fail is because the compiler cannot prove that the assertion is always true, for any and all instantiation of the template, it has nothing to do with how that function is called.

What I believe you are trying to do is to use a feature popularly called "SFINAE" (Substitution Failure Is Not An Error). But that only works for methods inside a template class/struct. (Read more about SFINAE here)

like image 2
Stian Svedenborg Avatar answered Nov 20 '22 11:11

Stian Svedenborg