使用具有默认参数的函数模板进行 decltype 会使结果混乱(一个有趣的问题或 gcc 的错误)

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

本文关键字:gcc 一个 问题 错误 参数 默认 函数模板 结果 decltype 混乱      更新时间:2023-10-16

为了直观地显示问题,您可以直接查看"更新"部分

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

考虑上面的代码,结果是合乎逻辑的。Beause每次调用getvalue函数都会添加一次state,就是有状态元编程.
但是,如果把getvalue(0, state<1>{});改成using t = decltype(getvalue(0, state<1>{}));,重用会很混乱。

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

上面的代码可以用g++来编译,这意味着state加了两次,这个结果相当混乱。为了解释为什么会有这样的结果。以下是我的猜测:

在#3处,要决定在默认的r使用哪个getvalue#1#2都被考虑,在实例化#1之前,generate_state<2>应该先实例化,所以state<2>被添加,之后,当#2被替换时没有falis,所以#2是state<2>的最佳匹配,然后添加了state<3>。这个过程不符合函数的重载规则(在正常的情况下,#1 和 #2 只选择一个,另一个从重载集中删除)。但除非是这样,否则这是不可能的。为什么?

为了展示编译器进程,添加static_assert,使编译器打印一些日志

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

为了简化问题,将代码分解如下:

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

两个函数模板getvalue都是实例化的,到底是怎么回事?在正常情况下,N=2 的decltype(create(state<N>{}))将被替换失败并从重载集中移除,只有具有 N=2 的decltype(create(state<N - 1>{}))U模板参数的函数模板才会被成功替换并由编译器实例化......

关于标准文档中带有默认参数的函数模板的引号:

如果函数模板 f 的调用方式需要使用默认参数,

则会查找依赖名称,检查语义约束,并实例化默认参数中使用的任何模板,就像默认参数是具有相同作用域的函数模板专用化中使用的初始值设定项一样, 与当时使用的函数模板 F 具有相同的模板参数和访问权限,除了声明闭包类型的范围 ([expr.prim.lambda.closure]) - 以及因此其关联的命名空间 - 仍然由默认参数的定义上下文确定。此分析称为默认参数实例化。然后,实例化的默认参数用作 f 的参数

更新:

这个问题可以进一步简化:

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

gcc 编译器将打印t = std::size_t.这意味着编译者选择了#22,但是在decltype(getvalue(0, state<2>{}))这一点上,create(state<2>{})的定义根本不存在,#22没有成功替换,它应该从overload set中删除,根据编译者打印的结果,它不是,这是多么令人惊讶!

如果将decltype(getvalue(0, state<2>{}));更改为getvalue(0, state<2>{})#11是最佳匹配并被实例化,这是符合逻辑的,因为此时尚未定义create(state<2>{}),因此#22将被替换失败,#11是最佳匹配。

是什么让结果如此混乱?有谁知道为什么?是 gcc 错误还是其他什么?

查看"更新"。

函数#11#22彼此重载。作为模板,它们都存在,并且在第一个参数上有所不同(intfloat)。因此getvalue(0, state<2>{})总是匹配#22,不管它是在什么表达式中(decltype或其他)。

例如:

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

编译和调用时:

$ g++ -std=c++17 main.cpp -o main && ./main | c++filt -t
unsigned long
unsigned long

如果您将#11修复为使用int,情况会变得更糟。编译器现在看到两个具有相同签名的模板函数,并引发一个不明确的调用错误:

main.cpp: In function ‘int main()’:
main.cpp:29:44: error: call of overloaded ‘getvalue(int, state<2>)’ is ambiguous
using t = decltype(getvalue(0, state<2>{}));
^
main.cpp:21:6: note: candidate: void getvalue(int, state<N>, int) [with int N = 2; U = state<1>]
void getvalue(int, state<N>, int res = generate_state<N>::value) {
^~~~~~~~
main.cpp:25:13: note: candidate: std::size_t getvalue(int, state<N>, int) [with int N = 2; U = state<2>; std::size_t = long unsigned int]
std::size_t getvalue(int, state<N>, int r = 0) {
^~~~~~~~

问题是 - 当您调用函数时,它会根据需要尝试实例化所有可能的替代方案,包括所有默认参数、默认模板参数等。当实例化后,替代方案有效时 - 它被考虑。

C++不可能仅仅因为尚未实例化带有参数的给定模板而拒绝替代方案。

可能的是拒绝替代方案,因为这种实例化失败了,正如Stian Svedenborg已经建议的那样。

关于可能性的快速示例:

#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>
struct is_zero{};
template<>
struct is_zero<0> {
using type = void;
};
//typename `is_zero<N>::type` is valid only for N=0,
//otherwise the expression leads to an error
template<int N>
struct is_nonzero{
using type = void;
};
template<>
struct is_nonzero<0> {
};
//typename `is_nonzero<N>::type` is valid for N!=0.
//For N=0 the expression leads to an error
template<int N, typename U = typename is_zero<N>::type > // #11
void getvalue(int, state<N>, int res = generate_state<N>::value) {
}
template<int N, typename U = typename is_nonzero<N>::type > // #22
std::size_t getvalue(int, state<N>, int r = 0) {
return N;
}
int main() {
//This tries to instantiate both #11 and #22.
//#11 leads to an error during default argument instantiation and is silently rejected.
//Thus #22 is used
using t = decltype(getvalue(0, state<2>{}));
std::cout << typeid(t).name() << std::endl;
//This also tries to instantiate both #11 and #22.
//#22 leads to an error during default argument instantiation and is silently rejected.
//Thus #11 is used
using u = decltype(getvalue(0, state<0>{}));
std::cout << typeid(u).name() << std::endl;
}

调用时,这将给出预期的结果:

$ g++ -std=c++17 main.cpp -o main && ./main | c++filt -t
unsigned long
void

一般来说,SFINAE - 允许在实例化期间静默拒绝错误的机制,而不是实际抛出错误并终止编译过程 - 真的很棘手。但是解释会很大,超出了这个问题/答案的范围。

更新:

了解问题:

这是一些有趣的代码!正如您在对我原始答案的评论中所述,这里的症结是state<N>类和generate_state<N>类中的friend auto声明。

如果我理解你的想法,重点是以这样一种方式声明类,即只有在generate_state<x>也在此范围内声明时才定义create(state<x>)

进一步深入研究您的代码,我相信我已经了解了正在发生的事情。

发生了什么事情

要了解正在发生的事情,让我们看一下您的第二个示例。

让我们将主要内容更改为以下内容:

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

这也编译和生成

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

这里发生的事情如下:

在第 1 行,#11 将无法解析,因为create(state<0>)不存在,这是替换失败,因此不是错误。 #22 将解析并因此使用。

在第 2 行,#11 将解决,在解决时解决generate_state<2>::value.此语句将create(state<2>)添加到编译器的符号表中。

在此之后,第 2 行将尝试解决 #22。直觉上,我们预计这会失败。但是,由于 #11 刚刚解决,create(state<2>)现在可用,#22 也解决了。intfloat匹配更好,因此选择了#22。

现在第 3 行也会发生同样的事情,因为create<(state<2>)可用。

如果您再次将 main 更改为以下内容,则会更加清楚:

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

因为这样做会导致编译器失败。

编译器失败,因为在(新)第 2 行上,create(state<2>) is not yet available, so #11 fails to resolve. As #11 fails to resolve,create(state<3>)' 永远不会添加到符号表中,因此 #22 也无法解析,从而导致编译错误。

同样,将 #11 中的默认参数更改为state<N>::value将导致 #11 被选取在 #22 上以进行get_value(0, state<2>)。如果这样做,除 1 和 2 以外的所有状态都将失败(如预期的那样)。


原答:保留解释评论。

在我看来,你的例子表现得符合预期。您似乎误解了有关模板实例化的基础知识。我将依次介绍它们:

当你写:

这意味着编译器选择了 #22,但在 decltype(getvalue(0, state<2>{})的这一点上,create(state<2>{}) 的定义根本不会 exsite

这种说法是错误的。模板类/结构的特征之一是,在需要时将声明类型。

这意味着该语句:

template struct generate_state<1>;

在这个例子中并没有真正做任何事情。您可以安全地删除它,代码仍将以完全相同的方式工作。使用上述语句的唯一原因是,当您希望在给定的编译单元中引用模板的某个版本(从而进行类型替换并写入代码)时。

我认为您误解的另一件事是模板函数的编译方式。

如您所知,在编写普通模板函数时,其调用有两个阶段。首先,在编译期间,替换模板参数并将函数写入代码。其次,当调用函数时,使用给定的参数执行先前编写的代码,通常这只发生在运行时,但是当调用函数是constexpr上下文时,该函数可以在编译时执行。

这是元编程的核心:设计在编译时执行的逻辑。元编程执行的输出是将要执行的代码。

因此,您的static_assert失败的原因是因为编译器无法证明断言始终为真,对于模板的任何和所有实例化,它与该函数的调用方式无关。

我相信您要做的是使用通常称为"SFINAE"的功能(替换失败不是错误)。但这仅适用于模板类/结构内的方法。(在此处阅读有关SFINAE的更多信息)

让我们只考虑"更新"部分。您依赖于一个非常危险的属性 - 类型系统计算的状态。也就是说,create(state<2>)保持未定义状态,直到一个看似无关的结构generate_state<2>被实例化。

任何受人尊敬的语言中的任何理智类型系统都是(或应该是)无状态的。给定的类型表达式是贯穿整个编译过程的常量。有了它,编译器可以使用复杂的推理算法来匹配类型并检查程序的正确性。

您使用的机制无视这一点。这种方法可能会导致非常奇怪的结果。一个完美的问题:有状态元编程是否格式不正确?显示它可能导致什么:

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

实际上被编译器接受!(点击上面的链接查看完整示例,我不想在这里复制粘贴)。

简而言之:不要使用它!如果您希望能够使用类型系统在不同的实现之间切换,请使用无状态 aproach,如我的另一个答案所示(我留作参考)。

遇到有状态类型计算时,不同的编译器似乎以不同的方式工作。你受他们内部的摆布。您的decltype场景显示了 g++ 实现的奇怪行为。似乎在decltype的上下文中,它实际上能够实例化auto create(state<N>),就好像它是一个独立的模板一样。

这使用 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

decltype(getvalue(0, state<2>{}))设法实例化create<2>,然后auto result = getvalue(0, state<2>{})使用#22成功编译。但是,如果注释掉前 2 行,则第 3 行会突然切换到#11并失败。

那么,标准对此有何规定?不多。可能是因为很难准确指定什么应该被认为是格式错误的。查看此答案以获取更详细的答案:https://stackoverflow.com/a/44268181/635654