Как правильно перегрузить оператор << для ostream?

237

Я пишу небольшую матричную библиотеку на C ++ для матричных операций. Однако мой компилятор жалуется, где раньше этого не было. Этот код оставлялся на полке в течение 6 месяцев, и между тем я обновил свой компьютер с debian etch до lenny (g ++ (Debian 4.3.2-1.1) 4.3.2), однако у меня та же проблема в системе Ubuntu с тем же g ++ ,

Вот соответствующая часть моего класса матрицы:

namespace Math
{
    class Matrix
    {
    public:

        [...]

        friend std::ostream& operator<< (std::ostream& stream, const Matrix& matrix);
    }
}

И «реализация»:

using namespace Math;

std::ostream& Matrix::operator <<(std::ostream& stream, const Matrix& matrix) {

    [...]

}

Это ошибка, выданная компилятором:

matrix.cpp: 459: ошибка: 'std :: ostream & Math :: Matrix :: operator << (std :: ostream &, const Math :: Matrix &)' должен принимать ровно один аргумент

Я немного сбит с толку этой ошибкой, но опять же мой C ++ стал немного ржавым после того, как я много работал за те 6 месяцев. :-)

Матиас ван дер Влис
источник

Ответы:

127

Вы объявили свою функцию как friend. Это не член класса. Вы должны удалить Matrix::из реализации. friendозначает, что указанная функция (которая не является членом класса) может обращаться к закрытым переменным-членам. То, как вы реализовали функцию, похоже на метод экземпляра для Matrixкласса, что неправильно.

Мехрдад Афшари
источник
7
И вы должны также объявить это внутри пространства имен Math (не только с использованием пространства имен Math).
Дэвид Родригес - dribeas
1
Почему они operator<<должны быть в пространстве имен Math? Кажется, это должно быть в глобальном пространстве имен. Я согласен, что мой компилятор хочет, чтобы он был в пространстве имен Math, но это не имеет смысла для меня.
Марк Лаката
Извините, но я не понимаю, почему мы тогда используем ключевое слово «друг»? Когда объявляется переопределение оператора друга в классе, кажется, что мы не можем реализовать с помощью Matrix :: operator << (ostream & os, const Matrix & m). Вместо этого нам нужно просто использовать глобальный оператор переопределения (<< ostream & os, const Matrix & m), так зачем вообще вообще объявлять его внутри класса?
Патрик
139

Просто скажу вам еще об одной возможности: мне нравится использовать для этого определения друзей:

namespace Math
{
    class Matrix
    {
    public:

        [...]

        friend std::ostream& operator<< (std::ostream& stream, const Matrix& matrix) {
            [...]
        }
    };
}

Функция будет автоматически нацелена на окружающее пространство имен Math(даже если ее определение присутствует в области действия этого класса), но не будет видимой, если вы не вызовете operator << с объектом Matrix, который сделает поиск с зависимым аргументом для поиска этого определения оператора. Иногда это может помочь с неоднозначными вызовами, поскольку он невидим для типов аргументов, отличных от Matrix. При написании его определения вы также можете напрямую ссылаться на имена, определенные в Matrix, и на саму Matrix, не квалифицируя имя с некоторым возможно длинным префиксом и предоставляя такие параметры шаблона, как Math::Matrix<TypeA, N>.

Йоханнес Шауб - Литб
источник
77

Чтобы добавить в ответ Mehrdad,

namespace Math
{
    class Matrix
    {
       public:

       [...]


    }   
    std::ostream& operator<< (std::ostream& stream, const Math::Matrix& matrix);
}

В вашей реализации

std::ostream& operator<<(std::ostream& stream, 
                     const Math::Matrix& matrix) {
    matrix.print(stream); //assuming you define print for matrix 
    return stream;
 }
кал
источник
4
Я не понимаю, почему это голосование против, это проясняет, что вы можете объявить оператора в пространстве имен, а не даже в качестве друга, и как вы можете объявить оператора.
Кал
2
Ответ Mehrdad не содержал никакого фрагмента кода, поэтому я просто добавил, что может сработать, переместив его за пределы класса в самом пространстве имен.
кал
Я понимаю вашу точку зрения, я только посмотрел на ваш второй фрагмент. Но теперь я вижу, что вы вывели оператора из класса. Спасибо за предложение.
Матиас ван дер Влис
7
Мало того, что из класса, но он правильно определен в пространстве имен Math. Кроме того, у него есть дополнительное преимущество (возможно, не для матрицы, но с другими классами), что «печать» может быть виртуальной, и, таким образом, печать будет происходить на самом производном уровне наследования.
Дэвид Родригес - dribeas
68

Предполагая, что мы говорим о перегрузке operator <<для всех классов, производных от класса, std::ostreamдля обработки Matrixкласса (а не о перегрузке <<для Matrixкласса), имеет смысл объявить функцию перегрузки вне пространства имен Math в заголовке.

Используйте функцию друга, только если функциональность не может быть достигнута через общедоступные интерфейсы.

Matrix.h

namespace Math { 
    class Matrix { 
        //...
    };  
}
std::ostream& operator<<(std::ostream&, const Math::Matrix&);

Обратите внимание, что перегрузка оператора объявляется вне пространства имен.

Matrix.cpp

using namespace Math;
using namespace std;

ostream& operator<< (ostream& os, const Matrix& obj) {
    os << obj.getXYZ() << obj.getABC() << '\n';
    return os;
}

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

math.h

namespace Math {
    class Matrix {
        public:
            friend std::ostream& operator<<(std::ostream&, const Matrix&);
    };
}

Вам нужно заключить определение функции в блок пространства имен, а не просто using namespace Math;.

Matrix.cpp

using namespace Math;
using namespace std;

namespace Math {
    ostream& operator<<(ostream& os, const Matrix& obj) {
        os << obj.XYZ << obj.ABC << '\n';
        return os;
    }                 
}
sanjivr
источник
38

В C ++ 14 вы можете использовать следующий шаблон для печати любого объекта, который имеет T :: print (std :: ostream &) const; член.

template<class T>
auto operator<<(std::ostream& os, T const & t) -> decltype(t.print(os), os) 
{ 
    t.print(os); 
    return os; 
} 

В C ++ можно использовать 20 концепций.

template<typename T>
concept Printable = requires(std::ostream& os, T const & t) {
    { t.print(os) };
};

template<Printable T>
std::ostream& operator<<(std::ostream& os, const T& t) { 
    t.print(os); 
    return os; 
} 
QuentinUK
источник
интересное решение! Один вопрос - где этот оператор должен быть объявлен, как в глобальной области видимости? Я предполагаю, что это должно быть видно для всех типов, которые могут быть использованы для его шаблонизации?
Барни
@barney Это может быть в вашем собственном пространстве имен вместе с классами, которые его используют.
QuentinUK
вы не можете просто вернуться std::ostream&, так как это тип возврата в любом случае?
Жан-Мишель Селерье
5
@ Jean-MichaëlCelerier Тип decl гарантирует, что этот оператор используется только при наличии t :: print. В противном случае он попытался бы скомпилировать тело функции и выдать ошибку компиляции.
QuentinUK
Добавлена ​​версия концепции, протестировано здесь godbolt.org/z/u9fGbK
QuentinUK