clang 6 和 clang 7 模板转换运算符区别

Template conversion operator difference between clang 6 and clang 7

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

简化后的代码如下所示:

#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

我认为这与 Bug 32861 and the original report 有关。 似乎已在 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 6 中扣除 T 将是 T=bar 这导致 std::is_same<T&&, U>::value 为真,但在 clang 7 中推导为 T=bar const,现在该特征不再成立, 重载不会添加到候选集中。

另请注意,在 clang 7 中推导为 T=bar const 的事实也会导致 !std::is_const<T>::value 为假,并有助于丢弃重载。

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 拒绝它。

在 Gcc 的情况下,T=intT=const int 都被视为案例。仅适用于 clang 7 T=const int。因为 T=const int 被禁用,clang 7 拒绝代码。

根据[over.match.ref]

The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “cv2 T2” or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where “cv1 T” is reference-compatible with “cv2 T2”, are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” or “cv2 T2” or “rvalue reference to cv2 T2”, respectively, where T2 is the same type as T or can be converted to type T with a qualification conversion, are also candidate functions.

在我们的例子中,这意味着可以将 S 转换为 int&const int&

[temp.deduct.conv]

Template argument deduction is done by comparing the return type of the conversion function template (call it P) with the type that is required as the result of the conversion (call it A; see [dcl.init], [over.match.conv], and [over.match.ref] for the determination of that type) as described in [temp.deduct.type].

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

  1. gcc认为转换的结果并不代表转换序列的结果,所以它先根据[over.match.ref]决定哪些转换序列是可接受的,然后对所有可能的转换序列进行转换算子的模板实参推导

  2. clang认为转换的结果确实意味着转换序列的目标。并且它只对 T=cont int.

  3. 执行参数推导

根据我在标准中阅读的内容,我无法说出标准的 "right" 解释是什么。尽管如此,我认为 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.
  }