Устранение неоднозначности двоичного оператора друга и члена

#c #language-lawyer

#c #g #clang #друг #неоднозначный вызов

Вопрос:

Рассмотрим следующий класс с двоичным оператором (я использую operator только в качестве примера).

 struct B{};

template<class>
struct A{
    template<class BB>
    void operator (BB constamp;) const{std::cout<<"member"<<std::endl;}
    template<class BB>
    friend void operator (BB constamp;, A constamp;){std::cout<<"friend"<<std::endl;}
};
  

Я могу вызвать этот двоичный оператор с двумя разными типами:

 A<int> a;
B b;
a   b; // member
b   a; // friend
  

Затем, когда я пытаюсь использовать A с обеих сторон ( a a ), происходит много странных вещей. Три компилятора дают разные ответы на один и тот же код.

Некоторый контекст: я не хочу определять void operator (A constamp;) , потому что мне нужен шаблон для удаления функций SFINAE, если какой-то синтаксис не работает. Также я не хочу template<class BB, class AA> friend void operator(BB constamp;, AA constamp;) . Поскольку since A является шаблоном, разные экземпляры будут создавать несколько определений одного и того же шаблона.

Продолжение работы с исходным кодом:

Странная вещь # 1: в gcc друг имеет приоритет:

 a   a; // prints friend in gcc
  

Я ожидаю, что член будет иметь приоритет, есть ли способ, чтобы член имел приоритет gcc?

Странная вещь # 2: в clang этот код не компилируется:

 a   a; // use of overload is ambiguous
  

Это уже указывает на несоответствие между gcc и clang, кто прав? Каким может быть обходной путь для clang, который заставляет его работать как gcc?

Если я попытаюсь быть более жадным в аргументах, например, для применения некоторой оптимизации, я мог бы использовать ссылки на пересылку:

 struct A{
    template<class BB>
    void operator (BBamp;amp;) const{std::cout<<"member"<<std::endl;}
    template<class BB>
    friend void operator (BBamp;amp;, A constamp;){std::cout<<"friend"<<std::endl;}
};
  

Странная вещь # 3: использование ссылки на пересылку выдает предупреждение в gcc,

 a   a; // print "friend", but gives "warning: ISO C   says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second:"
  

Но все равно компилируется, как я могу отключить это предупреждение в gcc или обходном пути?Как и в случае № 1, я ожидаю, что предпочту функцию-член, но здесь она предпочитает функцию friend и выдает предупреждение.

Странная вещь # 4: использование ссылки на пересылку выдает ошибку в clang.

 a   a; // error: use of overloaded operator ' ' is ambiguous (with operand types 'A' and 'A')
  

Что снова указывает на несоответствие между gcc и clang, кто прав в этом случае?

В общем, я пытаюсь заставить этот код работать последовательно. Я действительно хочу, чтобы в функцию была введена функция friend (а не бесплатные функции friend). Я не хочу определять функцию с равными аргументами, отличными от шаблона, потому что разные экземпляры будут создавать дублированные объявления одних и тех же функций.


Вот полный код, с которым можно поиграть:

 #include<iostream>
using std::cout;
struct B{};

template<class>
struct A{
    template<class BB>
    void operator (BB constamp; /*or BBamp;amp;*/) const{cout<<"membern";}
    template<class BB>
    friend void operator (BB constamp; /*or BB constamp;*/, A constamp;){cout<<"friendn";}
};

int main(){
    A<int> a;      //previos version of the question had a typo here: A a;
    B b;
    a   b; // calls member
    b   a; // class friend
    a   a; // surprising result (friend) or warning in gcc, hard error in clang, MSVC gives `member` (see below)

    A<double> a2; // just to instantiate another template
}
  

Примечание: я использую clang version 6.0.1 и g (GCC) 8.1.1 20180712 . По словам Фрэнсиса Куглера, MSVS 2017 CE дают еще другое поведение.


Я нашел обходной путь, который делает правильные вещи (печатает ‘member’ для a a регистра) как для clang, так и для gcc (для MSV?), Но для этого требуется много для котельной плиты и искусственного базового класса:

 template<class T>
struct A_base{
    template<class BB>
    friend void operator (BB constamp;, A_base<T> constamp;){std::cout<<"friend"<<std::endl;}
};

template<class T>
struct A : A_base<T>{
    template<class BB>
    void operator (BB constamp;) const{std::cout<<"member"<<std::endl;}
};
  

Однако он по-прежнему выдает неоднозначный вызов, если я заменяю BB constamp; на BBamp;amp; .

Ответ №1:

Все это неоднозначно. В GCC известны ошибки частичного упорядочения при упорядочении члена и нечлена, например https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66914 .

Просто ограничьте своего друга, чтобы он не участвовал в разрешении перегрузки, если BB это специализация A .

Комментарии:

1. Спасибо, но как вы делаете такое ограничение? Я попытался с помощью enable_if, но это не помогло, потому что ошибка возникает до устранения функции.

2. Эта ошибка указывает на дубликат gcc.gnu.org/bugzilla/show_bug.cgi?id=53499 , это примерно с 2012 года. Кажется, что особого интереса нет. Я бы сказал, что эта функция имеет фундаментальное значение для правильной реализации алгебраических типов.

Ответ №2:

Вы хотите отключить friend, если BB преобразуется в:

 template<
  class BB,
  std::enable_if<
    !std::is_convertible<B constamp;, A>::value, int>::type = 0>
friend void operator (BB constamp;, A constamp;){std::cout<<"friend"<<std::endl;}
  

Примечание: вам нужно использовать в std::enable_if качестве типа, чтобы сделать так, чтобы объявление функции никогда не разрешалось, если SFINAE не разрешает.

Еще один совет: если вы хотите разрешить функцию-член, если BB легко преобразуется в A (и не равно A), вы можете указать тип по умолчанию для шаблона:

 template <typename BB = A>
void operator  (BB constamp;) const {/*...*/}
  

Это действительно полезно только в том случае, если вы предоставляете классу другие операторы opterator , но это стоит отметить.

Комментарии:

1. Вы бы ограничили функцию friend или функцию-член?

2. Это зависит от того, что вы хотите выбрать. Я предполагаю, что вы захотите использовать use the member, когда BB==A , поэтому вы хотите отключить функцию friend, но это полностью зависит от вас :-). Еще один совет: вы можете захотеть, чтобы функция-член была шаблоном <class BB=A> , что поможет функции-члену разрешить, если BB можно преобразовать в A. Я добавлю это в свою правку.

3. Спасибо, я думал в случае, когда общий эффект friend и member одинаков. То есть случай, представляющий собой арифметическую операцию, в которой оба имеют одинаковый конечный эффект, но все же компиляторы требуют разрешения неоднозначности.

4. Что касается вашего редактирования, я не уверен, помогает ли аргумент шаблона по умолчанию с выводом или для устранения двусмысленности.

5. (другими словами, печальная часть этого обходного пути заключается в том, что я даже не хочу менять поведение, а просто устраняю двусмысленность. Я не знаю, почему функция-член не была бы предпочтительнее на данный момент и была бы выполнена с ней, как, по-видимому, MSVS, согласно другому ответу).

Ответ №3:

Я пошел и запустил ваш код в Visual Studio 2017 CE как:

 int main(){
    A<int> a;
    B b;
    a   b; // calls member
    b   a; // class friend
    a   a; // surprising result or warning in gcc, hard error in clang
}
  

И Visual Studio скомпилированы без ошибок и успешно запущены без предупреждений, и когда программа вернулась, она завершилась с кодом (0) с этим выводом:

 member
friend
member
  

Я пробовал это с помощью float, double, char и получил те же результаты.

Даже в качестве дополнительного теста я добавил это после класса шаблона выше:

 /* your code here */

struct C {};

int main() {
    A<C> a;
    B b;
    a   b;
    b   a;
    a   a;

    return 0;
}
  

И все равно получил те же результаты.


Что касается более поздней части вашего вопроса, которая относится к Strange thing #1, #2, #3, #4: тому, что относится к обоим gcc amp; clang

У меня не было этой проблемы с Visual Studio, поскольку a a я выдавал мне a member для вывода, и член имел приоритет над перегрузкой friend. Теперь, что касается факта приоритета оператора, я не знаю, насколько GCC и Clang будет отличаться от, Visual Studio поскольку каждый компилятор работает по-разному, и я с ними не знаком, но что касается самого языка, ваш компилятор не знает, что делать, A:: () когда он не знает, что <type> делатьиспользуйте. Однако он знает, что делать, когда у вас есть A<int>:: () или A<char>:: () или A<C>:: ()

Комментарии:

1. Да, извините, он не скопировал <int> in A<int> в примере. Я исправил это сейчас, так что вы можете удалить первую часть своего ответа. В любом случае MSVC демонстрирует третье поведение (и то, которое мы ожидали).

2. Я изменил ваш ответ, чтобы удалить ту часть, которая была вызвана моей ошибкой.

3. @alfC Не проблема и спасибо за помощь!