首页 > 解决方案 > 带有默认参数的函数模板的 decltype 会产生混淆的结果(一个有趣的问题或 gcc 的错误)

问题描述

为了直观的展示问题,可以直接看'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和for ,然后被添加。这个过程不符合函数的重载规则(正常情况下,#1和#2只选择一个,另一个从重载集中移除)。但除非是这样,否则这是不可能的。为什么?#2#1generate_state<2>state<2>state<2>state<3>

为了显示编译器的过程,添加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 将被替换失败并从重载集中删除,只有模板参数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 错误还是其他什么?

标签: c++templateslanguage-lawyer

解决方案


看着“更新”。

函数#11#22相互重载。作为模板,它们都存在,并且在第一个参数(intvs float)上有所不同。因此,无论它在(或其他)中的表达式如何,getvalue(0, state<2>{})都将始终匹配。#22decltype

例如:

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


推荐阅读