c++ - decltype 与具有默认参数的函数模板会产生困惑的结果(一个有趣的问题或 gcc 的错误)

标签 c++ templates language-lawyer

为了直观的展现问题,可以直接看“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函数将添加 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,决定哪个 getvalue用于默认参数 r ,两者#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都实例化了,什么鬼?正常情况下decltype(create(state<N>{})) N=2的函数模板会被替换失败,并从重载集中删除,只有模板参数U的函数模板的decltype(create(state<N - 1>{})) N=2 将被成功替换并由编译器实例化...

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

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

更新:

问题可以进一步简化:

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 bug 还是其他什么?

最佳答案

查看“更新”。

功能#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 - 允许在实例化期间默默拒绝错误的机制,而不是实际抛出错误并终止编译过程 - 确实很棘手。但解释会很大,超出了这个问题/答案的范围。

关于c++ - decltype 与具有默认参数的函数模板会产生困惑的结果(一个有趣的问题或 gcc 的错误),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59557940/

相关文章:

c++ - RtlpNtMakeTemporaryKey - VerySleepy 表示这需要很多时间

c++ - 使用 Armadillo 库进行意外(错误)的模板推导

c - 将 C 要求应用于未选择的 _Generic 案例

c++ - 保留名称和用户文字

c++ - Pimpl - 为什么可以在不完整的类型上调用 make_unique

c++ - 如何使用 std::cin 清除 TTY 设置为原始模式的输入缓冲区?

c++ - 同一 C++ Linux 应用程序中的 Protocol Buffers 2 和 3

c++ - 我可以使用 std::vector 作为模板参数还是必须是 std::vector<T>?

c++ - 重载模板函数的重新排序

c++ - 提取类的模板参数并迭代它们的最紧凑的方法是什么?