繁体   English   中英

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

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

为了直观的展示问题,可以直接看'UPDATE'部分

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

考虑上面的代码,结果是getvalue的。因为每次调用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时没有错误,所以#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都被实例化了,这是什么getvalue ?正常情况下, decltype(create(state<N>{}))与 N=2 将被替换失败并将从重载集中删除,只有带有模板参数Udecltype(create(state<N - 1>{}))与 N=2 将被成功替换并由编译器实例化...

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

如果以需要使用默认参数的方式调用函数模板 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表达式中( 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声明。

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

深入研究你的代码,我相信我已经明白发生了什么。

怎么了

要了解发生了什么,让我们看一下您的第二个示例。

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

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<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>{}) 的定义根本不存在

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

这意味着声明:

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

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

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

当遇到有状态类型计算时,不同的编译器似乎以不同的方式工作。 你是在他们的内部摆布。 您的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> ,然后使用#22成功编译auto result = getvalue(0, state<2>{}) 但是,如果您注释掉前 2 行,第 3 行会突然切换到#11并失败。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM