Template conversion operator difference between clang 6 and clang 7

255 Views Asked by At

I have some code that uses template conversion operator to find return type of function found through ADL.

The simplified code look like this:

#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>{});
}

In clang 6 and GCC, the above code compiles. However, in Clang 7, it doesn't compile anymore!

https://godbolt.org/z/Lfs3UH

As you can see, clang 6 resolve the call to probe<foo::bar>::operator foo::bar&&<foo::bar, foo::bar&&, 0>() but clang 7 fails because it tries to call probe<foo::bar>::operator const foo::bar&&<const foo::bar, foo::bar&&, 0>()

Which compiler is right? What is the rule in the standard for this? Is this a new Clang bug or is it a fix?


There are many cases I want to check. Not just foo::bar as parameter, but many reference types, such as this:

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&&>{});
}

Resolving to the right function call is important.

Here's a live example of all those cases, GCC succeed but clang fails: https://godbolt.org/z/yrDFMg

2

There are 2 best solutions below

0
On BEST ANSWER

The difference in behavior between clang 6/7 and gcc is illustrated by this simplified example code:

#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 and Clang 6 accept the code, and clang 7 rejects it.

In the case of Gcc both T=int and T=const int are considered cases. For clang 7 only T=const int. Because T=const int is disabled, clang 7 reject the code.

According to [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.

In our case this means that convertion of S to int& or const int& could be candidate.

And [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].

So I think two literal readings are acceptable:

  1. gcc considers that the result of the conversion does not mean the result of the conversion sequence, so it first decides that which conversion sequence are acceptable according to [over.match.ref] and then perform template argument deduction for the conversion operator for all the possible conversion sequences.

  2. clang considers that the result of the conversion does mean the target of the conversion sequence. And it performs argument deduction only for T=cont int.

From what I have read in the standard I cannot say what is the "right" interpretation of the standard. Nevertheless I think that clang behavior is more consistent with template argument deduction in general:

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.
  }
2
On

I believe this is related to Bug 32861 and the original report. which it seems has been solved in clang 7.

Taking for example the second conversion overload:

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&& ();

in clang 6 the deduction to T will be T=bar which causes std::is_same<T&&, U>::value be true, but in clang 7 the deduction would be T=bar const and now the trait no longer hold, the overload is not added to the candidate set.

Note also that the fact that in clang 7 the deduction is T=bar const will also cause !std::is_const<T>::value be false and also contribute to discard the overload.