Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template conversion operator difference between clang 6 and clang 7

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

like image 477
Guillaume Racicot Avatar asked Oct 04 '18 17:10

Guillaume Racicot


1 Answers

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.
  }
like image 177
Oliv Avatar answered Sep 22 '22 23:09

Oliv