Разница между оператором преобразования шаблонов между 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

2 ответа

Решение

Различие в поведении между 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=int а также T=const int считаются делами. Только для Clang 7 T=const int, Так как T=const int отключен, Clang 7 отклонить код.

Согласно [over.match.ref]:

Рассматриваются функции преобразования S и его базовые классы. Те неявные функции преобразования, которые не скрыты в S и дают тип "lvalue ссылка на cv2 T2" (при инициализации lvalue ссылка или rvalue ссылка на функцию) или "cv2 T2" или "rvalue ссылка на cv2 T2" (когда Инициализация ссылки на rvalue или ссылки на lvalue на функцию), где "cv1 T" совместима с "cv2 T2", являются кандидатами на функции. Для прямой инициализации те явные функции преобразования, которые не скрыты в S и дают тип "lvalue ссылка на cv2 T2" или "cv2 T2" или "rvalue ссылка на cv2 T2", соответственно, где T2 является тем же типом, что и T или могут быть преобразованы в тип T с преобразованием квалификации, также являются кандидатами функции.

В нашем случае это означает, что преобразование S в int& или же 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.
  }

Я считаю, что это связано с ошибкой 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 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 быть ложным, а также способствовать отмене перегрузки.

Другие вопросы по тегам