8

我有一些代码使用模板转换运算符来查找通过 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

4

2 回答 2

1

我相信这与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 6Twill be的推论中T=bar,原因 std::is_same<T&&, U>::value是真的,但在clang 7推论中T=bar const,现在该特征不再成立,重载不会添加到候选集中。

还要注意,在clang 7推论中的事实T=bar const也会导致!std::is_const<T>::value错误,也有助于丢弃过载。

于 2018-10-04T18:58:47.063 回答
1

这个简化的示例代码说明了 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被认为是情况。仅适用于铿锵 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 类型,也是候选函数。

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

[temp.deduct.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.
  }
于 2018-10-05T08:04:36.517 回答