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


person Guillaume Racicot    schedule 04.10.2018    source источник
comment
Странно, но если поменять std::is_same<T const&&, U> на std::is_same<T&&, U>, то будет работать. Я думаю, что это функция С++ 17. godbolt.org/z/ZO7pfC   -  person Victor Gubin    schedule 04.10.2018
comment
@VictorGubin да, но это своего рода читерство, в данном случае это не сработает: godbolt.org/z/0q4WOP< /а>   -  person Guillaume Racicot    schedule 04.10.2018
comment
Я удаляю ответ, потому что меня действительно смущает [temp.deduct.conv] и я понятия не имею, как применять эти правила, и они, вероятно, актуальны.   -  person Barry    schedule 04.10.2018


Ответы (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.
  }
person Oliv    schedule 05.10.2018

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

person Jans    schedule 04.10.2018
comment
Эм-м-м? это регресс со времен clang 7 - person Guillaume Racicot; 04.10.2018
comment
Нет, это исправление - person Jans; 04.10.2018