Подтвердить что ты не робот

Неявное преобразование при перегрузке операторов для классов шаблонов

Я хотел бы знать, почему неявное преобразование типов не работает с перегрузкой внешнего оператора на шаблонах классов. Вот рабочая, неглавная версия:

class foo
{
public:

    foo() = default;

    foo(int that)
    {}

    foo& operator +=(foo rhs)
    {
        return *this;
    }
};

foo operator +(foo lhs, foo rhs)
{
    lhs += rhs;
    return lhs;
}

Как и ожидалось, следующие строки компилируются правильно:

foo f, g;
f = f + g; // OK
f += 5; // OK
f = f + 5; // OK
f = 5 + f; // OK

С другой стороны, когда класс foo объявляется как простой шаблон следующим образом:

template< typename T >
class foo
{
public:

    foo() = default;

    foo(int that)
    {}

    foo& operator +=(foo rhs)
    {
        return *this;
    }
};

template< typename T >
foo< T > operator +(foo< T > lhs, foo< T > rhs)
{
    lhs += rhs;
    return lhs;
}

Следующие строки компилируются с ошибками:

foo< int > f, g;
f = f + g; // OK
f += 5; // OK
f = f + 5; // Error (no match for operator+)
f = 5 + f; // Error (no match for operator+)

Я хотел бы понять, почему компилятор (GCC 4.6.2) не может выполнить неявное преобразование типа, используя конструктор преобразования для версии шаблона класса. Это ожидаемое поведение? Помимо ручного создания всех необходимых перегрузок, существует ли какое-либо обходное решение для этого?

4b9b3361

Ответ 1

Причина, по которой он не просто работает, заключается в том, что неявные преобразования типов (то есть через конструкторы) не применяются при выводе аргумента шаблона. Но это работает, если вы делаете внешнего оператора другом с тех пор, как это известно типу T, позволяя компилятору исследовать, что можно сделать, чтобы аргументы совпадали.

Я сделал пример, основанный на вашем (но удаленный материал С++ 11), вдохновленный статьей 46 (класс рациональных чисел) в Scott Meyers Effective С++ (ред. 3). Ваш вопрос почти точно соответствует этому предмету. Скотт также отмечает, что... "это использование друга не связано с доступом к непубличным частям класса".

Это также позволит работать со смесями foo <T> , foo <U> и т.д., Если T и U могут быть добавлены и т.д.

Также посмотрите на это сообщение: Неопределенность перегрузки при добавлении С++

#include <iostream>

using namespace std;

template< class T >
class foo
{
private:
   T _value;
public:
   foo() : _value() {}

   template <class U>
   foo(const foo<U>& that) : _value(that.getval()) {}

   // I'm sure this it can be done without this being public also;
   T getval() const { return _value ; }; 

   foo(const T& that) : _value(that) {}

   friend const foo operator +(foo &lhs,const foo &rhs) 
      {
     foo result(lhs._value+rhs._value); 
     return result;
      };
   friend const foo operator +(foo &lhs,const T &rhsval) 
      {
     foo result(lhs._value+rhsval); 
     return result;
      };
   friend const foo operator +(const T &lhsval,foo &rhs) 
      {
     foo result(lhsval+rhs._value); 
     return result;
      };

   friend foo& operator +=(foo &lhs,const foo &rhs)
      {
     lhs._value+=rhs._value;
     return lhs;
      };   
   friend std::ostream& operator<<(std::ostream& out, const foo& me){
      return out <<me._value;
   }
};

int main(){
   foo< int > f, g;
   foo< double > dd;
   cout <<f<<endl;
   f = f + g;
   cout <<f<<endl;
   f += 3 ;
   cout <<f<<endl;
   f = f + 5;
   cout <<f<<endl;
   f = 7 + f; 
   cout <<f<<endl;      
   dd=dd+f;
   cout <<dd<<endl;      
   dd=f+dd;
   cout <<dd<<endl;      
   dd=dd+7.3;
   cout <<dd<<endl;             
}

Ответ 2

Я задал этот вопрос авторам библиотеки в MS и получил чрезвычайно информативный ответ от Стефана Лававей, поэтому я даю ему полную оценку этой информации.

Ошибка компиляции, которую вы получаете в случае шаблона, связана с тем, что вывод аргумента шаблона работает до разрешения перегрузки, а вывод аргумента шаблона требует точных совпадений, чтобы добавить что-либо к набору перегрузки.

Подробно вычитание аргумента шаблона рассматривает каждую пару типа параметра P и тип аргумента A и пытается найти замены шаблонов, которые сделают A точно совпадающим с P. После поиска совпадений для каждого аргумента он проверяет соответствие (так что если вы вызываете bar(foo<T>, foo<T>) с T = int для первого параметра, а T = double как второй, он также терпит неудачу). Только после того, как точные, согласованные совпадения будут успешно заменены в сигнатуре функции, это то, что подпись добавлена ​​в набор функций-кандидатов для разрешения перегрузки.

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

В случае operator+(foo<T>, foo<T>) с foo<int> + 5 вывод аргумента шаблона не может найти подстановки для T, что сделает выражение foo<T> в точности совпадающим с int, так что перегрузка оператора + будет выбрана как кандидат, а неявное преобразование никогда не наблюдается.

Похоже, что это, как правило, хорошо, поскольку оно делает шаблоны гораздо более предсказуемыми, оставляя область странных неявных поведений для перегрузки разрешения.

В стандарте есть что сказать об этом по адресу:

14.8.2.1 Вывод аргументов шаблона из вызова функции

"Вывод аргумента шаблона производится путем сравнения каждого параметра параметра шаблона функции (вызов его P) с помощью тип соответствующего аргумента вызова (назовите его A), как описано ниже....

... В общем, процесс дедукции пытается найти значения аргументов шаблона, которые сделают вывод A идентичный A (после преобразования типа A, как описано выше) "

Далее перечисляются несколько особых случаев, когда это правило имеет исключения, связанные с cv-квалификаторами (поэтому T & будет совместим с const T &) и сопоставлением производных классов (в некоторых случаях он может соответствовать Derived & Base &;), но в основном, точное соответствие является правилом.

Ответ 3

Все возможные foo<T> равны действительным преобразованиям из int, поскольку конструктор принимает int, а не тип шаблона. Компилятор не может использовать другой параметр в операторе, чтобы угадать, какой из них вы можете иметь в виду, поэтому вы получите ошибку. Если вы явно укажете, какой экземпляр вы хотите, я верю, что это сработает.