模板转换运算符在 clang 6 和 clang 7 之间的区别



我有一些代码使用模板转换运算符来查找通过 ADL 找到的函数的返回类型。

简化的代码如下所示:

#include <type_traits>
template<typename S>
struct probe {
template<typename T, typename U = S, std::enable_if_t<
std::is_same<T&, U>::value &&
!std::is_const<T>::value, int> = 0>
operator T& ();
template<typename T, typename U = S&&, std::enable_if_t<
std::is_same<T&&, U>::value &&
!std::is_const<T>::value, int> = 0>
operator T&& ();
template<typename T, typename U = S, std::enable_if_t<
std::is_same<T const&, U>::value, int> = 0>
operator T const& () const;
template<typename T, typename U = S&&, std::enable_if_t<
std::is_same<T const&&, U>::value, int> = 0>
operator T const&& () const;
};
namespace foo {
struct bar {};
auto find_me(bar const&) -> int { return 0; } 
}
int main() {
// That would be inside a template in my code.
find_me(probe<foo::bar>{});
}

在 clang 6 和 GCC 中,上面的代码编译。但是,在 Clang 7 中,它不再编译了!

https://godbolt.org/z/Lfs3UH

如您所见,clang 6 解析对probe<foo::bar>::operator foo::bar&&<foo::bar, foo::bar&&, 0>()的调用,但 clang 7 失败,因为它尝试调用probe<foo::bar>::operator const foo::bar&&<const foo::bar, foo::bar&&, 0>()

哪个编译器是正确的?标准中的规则是什么?这是一个新的 Clang 错误还是修复?


我想检查很多情况。不仅foo::bar参数,而且许多引用类型,例如:

namespace foo {
struct bar {};
auto find_me(bar const&) -> int { return 0; } 
auto find_me(bar&&) -> int { return 0; } 
auto find_me(bar const&&) -> int { return 0; } 
auto find_me(bar&) -> int { return 0; } 
}
int main() {
find_me(probe<foo::bar>{});
find_me(probe<foo::bar&>{});
find_me(probe<foo::bar&&>{});
find_me(probe<foo::bar const&>{});
find_me(probe<foo::bar const&&>{});
}

解析为正确的函数调用很重要。

以下是所有这些案例的活生生的例子,GCC 成功但 clang 失败:https://godbolt.org/z/yrDFMg

clang 6/7 和 gcc 之间的行为差异由以下简化的示例代码说明:

#include <type_traits>
struct S{
template<class T,class=std::enable_if_t<!std::is_const_v<T>>>
operator T& ();
};
void test() {
S a;
const int& i = a; //Accepted by Gcc and clang 6 accept, rejected by clang 7
}

GCC 和 Clang 6 接受代码,Clang 7 拒绝它。

在海湾合作委员会的情况下,T=intT=const int都被视为案例。对于叮当 7 只有T=const int.由于禁用T=const int,因此 clang 7 拒绝该代码。

根据[over.match.ref]:

考虑了 S 及其基类的转换函数。 那些未隐藏在 S 中的非显式转换函数,并且生成类型为"对 cv2 T2 的左值引用"(初始化对函数的左值引用或右值引用时(或"cv2 T2"或"对 cv2 T2 的右值引用"(初始化对函数的右值引用或对函数的左值引用时(,其中"cv1 T"与"cv2 T2"引用兼容, 是候选函数。 对于直接初始化,那些未隐藏在 S 中的显式转换函数和分别的生成类型"对 cv2 T2 的左值引用"或"cv2 T2"或"对 cv2 T2 的右值引用",其中 T2 与 T 的类型相同,或者可以通过限定转换转换为类型 T,也是候选函数。

在我们的例子中,这意味着将S转换为int&const int&可能是候选的。

和 [温度扣除.conv]:

模板

参数推导是通过将转换函数模板的返回类型(称为 P(与转换结果所需的类型(调用 A;请参阅 [dcl.init]、[over.match.conv] 和 [over.match.ref] 以确定该类型(来完成的,如 [temp.deduct.type] 中所述。

所以我认为两个字面解读是可以接受的:

  1. gcc 认为转换的结果并不意味着转换序列的结果,因此它首先根据 [over.match.ref] 确定哪个转换序列是可接受的,然后对所有可能的转换序列执行转换运算符的模板参数推导。

  2. Clang认为转换的结果确实意味着转换序列的目标。并且它仅对T=cont int执行参数推导。

从我在标准中读到的内容来看,我不能说什么是对标准的"正确"解释。尽管如此,我认为 clang 行为通常与模板参数推导更一致:

template<class T,class=std::enable_if_t<std::is_const_v<T>>>
void f(T& x);
void test(){
int i;
f(i);
// If considering that the argument type is int caused
// template argument deduction failure, then template argument
// deduction would be performed for a const int argument.
// But template argument deduction succeeds. So T is deduced to int. 
// Only after this deduction template argument substitution happens.
// => both gcc and clang reject this code.
}

我相信这与 Bug 32861 和原始报告有关。 这似乎已经在clang 7中解决了.

以第二个转换重载为例:

template<typename T, typename U = S&&, std::enable_if_t<
std::is_same<T&&, U>::value &&
!std::is_const<T>::value, int> = 0>
operator T&& ();

clang 6T的扣除额将被T=bar导致std::is_same<T&&, U>::value真的,但在clang 7中,推论将是T=bar const的,现在这个特征不再成立, 重载不会添加到候选集中。

另请注意,在clang 7扣除T=bar const的事实也会导致!std::is_const<T>::value错误,也会导致丢弃超载。

最新更新