Чи повинен оператор << реалізовуватися як друг або як член-функція?


129

Це в основному питання, чи є "правильний" спосіб реалізації operator<<? Читаючи це, я можу побачити щось подібне:

friend bool operator<<(obj const& lhs, obj const& rhs);

вважається за краще щось подібне

ostream& operator<<(obj const& rhs);

Але я не можу зовсім зрозуміти, чому я повинен використовувати те чи інше.

Моя особиста справа:

friend ostream & operator<<(ostream &os, const Paragraph& p) {
    return os << p.to_str();
}

Але я, мабуть, міг би зробити:

ostream & operator<<(ostream &os) {
    return os << paragraph;
}

На якому обґрунтуванні я повинен грунтуватися на цьому рішенні?

Примітка :

 Paragraph::to_str = (return paragraph) 

де абзац - рядок.


4
До речі, вам, мабуть, слід додати const до підписів функцій учасників
Motti

4
Чому повертати bool від оператора <<? Ви використовуєте це як оператор потоку або як перевантаження побітового зсуву?
Мартін Йорк,

Відповіді:


120

Проблема тут полягає у Вашій інтерпретації статті, яку Ви посилаєте .

Рівність

У цій статті йдеться про те, хто має проблеми з правильним визначенням операторів взаємозв'язку bool.

Оператор:

  • Рівність == і! =
  • Відносини <> <=> =

Ці оператори повинні повернути bool, порівнюючи два об'єкти одного типу. Зазвичай найпростіше визначити цих операторів як частину класу. Це пояснюється тим, що клас автоматично є другом, тому об'єкти типу «Абзац» можуть оглядати один одного (навіть один одного приватні члени).

Є аргумент для створення цих вільно стоячих функцій, оскільки це дозволяє автоматичному перетворенню конвертувати обидві сторони, якщо вони не одного типу, тоді як функції членів дозволяють автоматично перетворювати резус. Я вважаю це аргументом паперової людини, оскільки ви не дуже хочете, щоб автоматична конверсія відбувалася в першу чергу (як правило). Але якщо цього ви хочете (я не рекомендую), то зробити компаратори вільно стоячи можуть бути вигідними.

Потокове

Оператори потоку:

  • оператор << вихід
  • оператор >> введення

Коли ви використовуєте їх як оператори потоку (а не двійковий зсув), першим параметром є потік. Оскільки у вас немає доступу до об’єкта потоку (його не слід змінювати), вони не можуть бути операторами-членами, вони повинні бути зовнішніми для класу. Таким чином, вони повинні або бути друзями класу, або мати доступ до публічного методу, який здійснюватиме потокове передавання для вас.

Також для цих об'єктів традиційно повертати посилання на об'єкт потоку, щоб ви могли ланцюгові операції потоку разом.

#include <iostream>

class Paragraph
{
    public:
        explicit Paragraph(std::string const& init)
            :m_para(init)
        {}

        std::string const&  to_str() const
        {
            return m_para;
        }

        bool operator==(Paragraph const& rhs) const
        {
            return m_para == rhs.m_para;
        }
        bool operator!=(Paragraph const& rhs) const
        {
            // Define != operator in terms of the == operator
            return !(this->operator==(rhs));
        }
        bool operator<(Paragraph const& rhs) const
        {
            return  m_para < rhs.m_para;
        }
    private:
        friend std::ostream & operator<<(std::ostream &os, const Paragraph& p);
        std::string     m_para;
};

std::ostream & operator<<(std::ostream &os, const Paragraph& p)
{
    return os << p.to_str();
}


int main()
{
    Paragraph   p("Plop");
    Paragraph   q(p);

    std::cout << p << std::endl << (p == q) << std::endl;
}

19
Чому саме operator<< private:?
Метт Кларксон

47
@MattClarkson: Його немає. Таким чином, його декларація про функцію friend не є частиною класу і, таким чином, не впливає на специфікатори доступу. Я, як правило, розміщую декларації функції друзів поруч із даними, до яких вони отримують доступ.
Мартін Йорк

12
Чому для доступу до даних потрібна зручна функція, якщо ви використовуєте загальнодоступну функцію? Вибачте, якщо питання дурне.
Семен Данилов

4
@SemyonDanilov: Чому б ви порушили інкапсуляцію та додали гетерів! freiendце спосіб розширити публічний інтерфейс, не порушуючи інкапсуляцію. Читайте програмісти.stackexchange.com/
Мартін Йорк

3
@LokiAstari Але це, безумовно, аргумент для того, щоб або видалити to_str, або зробити його приватним. На даний момент оператор потокової передачі не повинен бути другом, оскільки він використовує лише загальнодоступні функції.
deworde

53

Ви не можете виконувати це як функцію-члена, тому що неявний thisпараметр є лівою частиною <<-оператора. (Отже, вам потрібно буде додати його як функцію-члена до ostream-класу. Не добре :)

Чи можете ви зробити це як безкоштовну функцію, не використовуючи friendїї? Саме тому я віддаю перевагу, тому що це дає зрозуміти, що це інтеграція ostream, а не основна функціональність вашого класу.


1
"не є основним функціоналом вашого класу." Ось що означає «друг». Якби це була основна функціональність, це був би клас, а не друг.
xaxxon

1
@xaxxon Я думаю, що моє перше речення пояснює, чому в цьому випадку неможливо додати функцію як функцію-члена. friendФункція має ті ж права, що і функції члена ( це те , що friendозначає), так як користувач класу, я б задатися питанням , чому це потрібно було б що. Це відмінність, яку я намагаюся зробити, формулюючи "основну функціональність".
Магнус Хофф

32

По можливості, як функції, які не є членами та не є друзями.

Як описали Герб Саттер та Скотт Мейєрс, віддайте перевагу функціям, що не належать до друзів, ніж функціям членів, щоб сприяти збільшенню інкапсуляції.

У деяких випадках, як-от потоки C ++, у вас не буде вибору, і ви повинні використовувати функції, які не є членами.

Але все-таки це не означає, що ви повинні робити ці функції друзями своїх класів: Ці функції все ще можуть отримати доступ до вашого класу через ваші класові аксесуари. Якщо вам вдасться записати ці функції таким чином, ви виграли.

Про прототипи операторів << і >>

Я вважаю, що приклади, які ви навели у своєму запитанні, помиляються. Наприклад;

ostream & operator<<(ostream &os) {
    return os << paragraph;
}

Я навіть не можу почати думати, як цей метод може працювати в потоці.

Ось два способи реалізації операторів << і >>.

Скажімо, ви хочете використовувати потоковий об’єкт типу Т.

І що ви хочете витягнути / вставити з / в T відповідні дані вашого об'єкта типу абзацу.

Прототипи функціональних операторів << і >>

Перша істота як функції:

// T << Paragraph
T & operator << (T & p_oOutputStream, const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return p_oOutputStream ;
}

// T >> Paragraph
T & operator >> (T & p_oInputStream, const Paragraph & p_oParagraph)
{
   // do the extraction of p_oParagraph
   return p_oInputStream ;
}

Загальні прототипи операторів << і >>

Друге - як методи:

// T << Paragraph
T & T::operator << (const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return *this ;
}

// T >> Paragraph
T & T::operator >> (const Paragraph & p_oParagraph)
{
   // do the extraction of p_oParagraph
   return *this ;
}

Зауважте, що для використання цього позначення необхідно розширити декларацію класу T. Для STL-об'єктів це неможливо (ви не повинні їх змінювати ...).

А що, якщо T - потік C ++?

Ось прототипи тих же операторів << і >> для потоків C ++.

Для загальних basic_istream та basic_ostream

Зауважте, що у випадку потоків, оскільки ви не можете змінити потік C ++, ви повинні реалізувати функції. Що означає щось на кшталт:

// OUTPUT << Paragraph
template <typename charT, typename traits>
std::basic_ostream<charT,traits> & operator << (std::basic_ostream<charT,traits> & p_oOutputStream, const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return p_oOutputStream ;
}

// INPUT >> Paragraph
template <typename charT, typename traits>
std::basic_istream<charT,traits> & operator >> (std::basic_istream<charT,traits> & p_oInputStream, const CMyObject & p_oParagraph)
{
   // do the extract of p_oParagraph
   return p_oInputStream ;
}

Для char istream та ostream

Наступний код буде працювати лише для потоків на основі char.

// OUTPUT << A
std::ostream & operator << (std::ostream & p_oOutputStream, const Paragraph & p_oParagraph)
{
   // do the insertion of p_oParagraph
   return p_oOutputStream ;
}

// INPUT >> A
std::istream & operator >> (std::istream & p_oInputStream, const Paragraph & p_oParagraph)
{
   // do the extract of p_oParagraph
   return p_oInputStream ;
}

Ріс Улеріч прокоментував той факт, що кодовий код є лише "спеціалізацією" загального коду над ним. Звичайно, Ріс має рацію: я не рекомендую використовувати приклад на основі char. Тут подано лише тому, що читати простіше. Оскільки він життєздатний лише в тому випадку, якщо ви працюєте лише з потоками на основі char, вам слід уникати цього на платформах, де звичайний код wchar_t (тобто в Windows).

Сподіваюсь, це допоможе.


Невже ваш загальний шаблон шаблону basic_istream та basic_ostream вже не охоплює версії std :: ostream- та std :: istream, оскільки два останні є лише примірниками колишнього використання символів?
Rhys Ulerich

@Rhys Ulerich: Звичайно. Я використовую лише загальну, шаблоновану версію, хоча б тому, що в Windows ви маєте справу з кодом char і wchar_t. Єдина заслуга другої версії - виглядати як простіша, ніж перша. Я про це уточню свій пост.
paercebal

10

Він повинен реалізовуватися як безкоштовні, недружні функції, особливо якщо, як і більшість речей сьогодні, вихід використовується в основному для діагностики та реєстрації даних. Додайте const-аксесуари для всіх речей, які потрібно зайти у висновок, а потім виведіть програму просто зателефонуйте їм та виконайте форматування.

Я фактично взявся за збір усіх цих вільних функцій вихідного потоку у заголовку та файлі реалізації "ostreamhelpers", але він зберігає цю вторинну функціональність далеко від реальної мети класів.


7

Підпис:

bool operator<<(const obj&, const obj&);

Здається, досить підозріло, це не відповідає streamконвенції, ні побітній конвенції, тому це виглядає як випадок, коли оператор перевантажує зловживання, operator <повинен повернутися, boolалеoperator << ймовірно, повинен повернути щось інше.

Якщо ви мали на увазі так, скажіть:

ostream& operator<<(ostream&, const obj&); 

Тоді оскільки ви не можете додавати функції до ostreamнеобхідності, функція повинна бути вільною функцією, незалежно friendвід того, залежить вона від того, до чого вона має доступ (якщо вона не потребує доступу до приватних або захищених членів, її не потрібно робити друг).


Варто згадати, що доступ до зміни ostreamбуде потрібний під час використання ostream.operator<<(obj&)замовлення; звідси вільна функція. В іншому випадку для доступу доступу повинен бути тип пари.
wulfgarpro

2

На закінчення хочеться додати, що ви дійсно можете створити оператора ostream& operator << (ostream& os)всередині класу, і він може працювати. З того, що я знаю, використовувати це не дуже добре, оскільки це дуже перекручено і неінтуїтивно.

Припустимо, у нас є цей код:

#include <iostream>
#include <string>

using namespace std;

struct Widget
{
    string name;

    Widget(string _name) : name(_name) {}

    ostream& operator << (ostream& os)
    {
        return os << name;
    }
};

int main()
{
    Widget w1("w1");
    Widget w2("w2");

    // These two won't work
    {
        // Error: operand types are std::ostream << std::ostream
        // cout << w1.operator<<(cout) << '\n';

        // Error: operand types are std::ostream << Widget
        // cout << w1 << '\n';
    }

    // However these two work
    {
        w1 << cout << '\n';

        // Call to w1.operator<<(cout) returns a reference to ostream&
        w2 << w1.operator<<(cout) << '\n';
    }

    return 0;
}

Отже, підводячи підсумок - ви можете це зробити, але ви, мабуть, не повинні :)


0

friend operator = рівні права як клас

friend std::ostream& operator<<(std::ostream& os, const Object& object) {
    os << object._atribute1 << " " << object._atribute2 << " " << atribute._atribute3 << std::endl;
    return os;
}

0

operator<< реалізована як друга функція:

#include <iostream>
#include <string>
using namespace std;

class Samp
{
public:
    int ID;
    string strName; 
    friend std::ostream& operator<<(std::ostream &os, const Samp& obj);
};
 std::ostream& operator<<(std::ostream &os, const Samp& obj)
    {
        os << obj.ID<<   << obj.strName;
        return os;
    }

int main()
{
   Samp obj, obj1;
    obj.ID = 100;
    obj.strName = "Hello";
    obj1=obj;
    cout << obj <<endl<< obj1;

} 

ВИХІД:
100 Привіт
100 Привіт

Це може бути функцією друга лише тому, що об'єкт знаходиться в правій частині, operator<<а аргумент cout- у лівій частині. Отже, це не може бути функцією члена класу, це може бути лише функція друга.


я не думаю, що є спосіб написати це як функцію члена !!
Rohit Vipin Mathews

Чому все сміливо. Дозвольте мені зняти це.
Себастьян Мах
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.