Variadic 模板参数包扩展失去限定符

Variadic template parameter pack expansion looses qualifier

假设我有一个可变参数函数模板,该模板采用指向具有所述可变参数的函数的函数指针。以下代码在gcc(11.2)下编译不通过,但在clang和msvc下编译通过(https://godbolt.org/z/TWbEKWb9f).

#include <type_traits>

void dummyFunc(const int);

template<typename... Args>
void callFunc(void(*)(Args...), Args&&...); // <- this one is problematic
// see 
template<typename... Args>
void callFunc2(void(*)(std::conditional_t<std::is_const_v<Args>, const Args, Args>...), Args&&...); // <- this one works


int main()
{
    // fails on gcc, works on clang and msvc
    callFunc<const int>(&dummyFunc, 2);
    // this works
    //callFunc(&dummyFunc, 2);
    // this works as well
    //callFunc2<const int>(&dummyFunc, 2);
}

将函数参数 'Args...' 明确指定为 'const int' 可防止 gcc 编译代码。显然,模板参数扩展在 gcc 下失去了 cv 限定符,而在 clang 和 msvc 下保留了它。错误信息是:

<source>: In function 'int main()':
<source>:15:24: error: no matching function for call to 'callFunc<const int>(void (*)(int), int)'
   15 |     callFunc<const int>(&dummyFunc, 2);
      |     ~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~
<source>:6:6: note: candidate: 'template<class ... Args> void callFunc(void (*)(Args ...), Args&& ...)'
    6 | void callFunc(void(*)(Args...), Args&&...);
      |      ^~~~~~~~
<source>:6:6: note:   template argument deduction/substitution failed:
<source>:15:24: note:   types 'const int' and 'int' have incompatible cv-qualifiers
   15 |     callFunc<const int>(&dummyFunc, 2);
      |     ~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~

我发现 'solution' 通过利用类型特征使其在此处编译 (),但我要么必须使用此方法,要么不使用此方法,而与编译器无关。我知道我不必明确指定模板参数,但我想了解为什么 gcc 无法编译代码。哪个编译器是“正确的”,即这段代码是否应该编译(即,是否应该删除 cv 限定符)?

作为旁注,如果我改用 const 引用,它也会在 gcc 下编译,并且模板参数被正确识别为 'const int&'(因为编译器绝对必须这样做)。我知道这样一个事实,如果模板参数是由编译器推导出来的并且没有明确指定,那么 cv-qualifier 将被删除。但在我看来 gcc 在这里的行为是错误的,因为我明确说明了要使用的类型。但是,我对标准的了解还不足以判断 gcc 或其他两个编译器是否不遵循该标准。它似乎与可变参数模板有关而不是模板推导本身,因为没有可变参数模板的版本在gcc下工作(https://godbolt.org/z/qn8a5bh5E):

void dummyFunc(const int);

template<typename Arg>
void callFunc(void(*)(Arg), Arg&&);

int main()
{
    callFunc<const int>(dummyFunc, 2);
}

此外,为什么自动模板类型推导甚至在第一种(有问题的)情况下也有效?由于函数指针,'Args...' 应推导为 'const int',由于第二个参数,应推导为 'int'。我的猜测是,在这种情况下,'Args...' 被推断为 'const int'(否则将无法编译)。我的猜测正确吗?如果有人能提示我参考标准中的相关部分,那就太好了。

cv-qualifier 应该总是被删除(无论是在确定 dummyFunc 的类型还是在将推导的参数替换为 callFunc 签名时),我很确定所有编译器同意这一点。这不是真正的问题。让我们稍微改变一下示例:

template <class... Args> struct S {
    template <class... T> S(T...) {}
};

template<typename... Args>
void callFunc2(S<Args...>);

int main()
{
    callFunc2<const int>(S<int>{});
}

现在GCC and Clang reject the code while MSVC accepts it.

GCC 和 Clang 都存在 const int(明确指定)和 int(推断)之间不匹配的问题,而 MSVC 显然很乐意让 Args = [const int] 按照规定。谁是对的?

据我所知,这里的问题是 [temp.arg.explicit]/9,其中指出:

Template argument deduction can extend the sequence of template arguments corresponding to a template parameter pack, even when the sequence contains explicitly specified template arguments.

因此,为包 Args 指定显式模板参数不会阻止推导。编译器仍然必须尝试推断 Args 以防需要扩展它以便将函数参数类型与参数类型相匹配。

从来没有明确解释过这一段标准的真正含义。我猜想 MSVC 的方法可能类似于“推导包就好像没有明确指定的模板参数,然后如果显式指定的模板参数不是推导的模板参数的前缀则抛出结果”这似乎是一种明智的方式来处理这段代码。 GCC 和 Clang 可能类似于“像没有显式指定的模板参数一样推导包,然后如果显式指定的模板参数不是推导的模板参数的前缀则失败”,这将导致代码为 ill-formed,但这似乎是一种不幸的解释,因为它与 non-variadic 案例中明确指定的模板参数的处理方式不一致。

上面的示例类似于 OP 示例的简化版本:

void dummyFunc(int);

template<typename... Args>
void callFunc(void(*)(Args...));

int main()
{
    callFunc<const int>(&dummyFunc);
}

这里,尾随 Args&&... 已被删除,这不会改变结果:与 OP 的代码一样,Clang and MSVC accept it while GCC doesn't.。只有 Clang 改变了看法:它接受这个,拒绝 S 的那个。公平地说,这两个片段并不完全相似:带有 S 的片段涉及隐式转换。但不清楚为什么 Clang 会区别对待它们。

在我看来,GCC 和 Clang 在可变参数模板推导方面都有不同的错误,而 MSVC 在这两种情况下都做对了。但很难根据标准文本做出明确的论证。