Могут ли средства вставки и извлечения iostream быть членами класса вместо глобальных перегрузок?

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

(Примечание: если у кого-то есть лучший Google-Fu, чтобы найти уже написанный хороший ответ, мне было бы интересно это прочитать.)

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

Rational r (1, 2);
cout << "Your rational number is " << r;

Вы должны были бы написать строку вывода как:

r >> ("Your rational number is " >> cout);

Круглые скобки нужны, чтобы начать обратную цепочку, потому что >> и << связаны слева направо. Без них он попытается найти совпадение для r >> "Your rational number is " до "Your rational number is " >> cout. Был оператор с ассоциативностью справа налево был выбран, этого можно было бы избежать:

r >>= "Your rational number is " >>= cout;

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

Но является ли это пределом, и такое обращение практически неизбежно для любого дизайна в стиле iostream, который хотел бы, чтобы сериализация была отправлена ​​в класс? Я пропустил какие-либо другие проблемы?


ОБНОВЛЕНИЕ Возможно, лучше сформулировать «проблему» так: я подозреваю следующее:

Для непотоковых объектов, которые хотят сериализоваться, библиотека iostream гипотетически МОЖЕТ быть спроектирована таким образом, чтобы средства вставки и извлечения были членами класса, а не глобальными перегрузками... и без (значительного) влияния на свойства времени выполнения. Тем не менее, это сработало бы ТОЛЬКО в том случае, если бы авторы iostream были готовы признать, что это заставит клиентов формировать потоковые операции справа налево.

Но мне не хватает интуиции о том, почему глобальная перегрузка оператора по сравнению с членом может разблокировать неразблокируемую в противном случае способность выражать себя слева направо (вместо справа налево). Вопрос в том, могут ли ретроспективные взгляды, шаблоны или какая-то эзотерическая особенность C++11 предложить альтернативу. ИЛИ имеет ли «физика C++» врожденный уклон в сторону одного направления по сравнению с другим, и глобальная перегрузка каким-то образом является единственным трюком во время компиляции в книге для его переопределения.

Сравните с правилом левой руки Флеминга для двигателей


person HostileFork says dont trust SE    schedule 22.10.2011    source источник
comment
Необходимость читать аргументы справа налево уже достаточно плоха. Может быть, это поможет: stackoverflow.com/questions/7052568/ :)   -  person UncleBens    schedule 23.10.2011
comment
@UncleBens Это интересный момент. Хотя я говорил о достижении поведения во время выполнения, эквивалентного существующим iostreams, при этом разрешая непотоковым классам определять ›› и ‹‹ (или я бы принял другие операторы) как функции-члены вместо глобальных друзей. Ваш метод означает, что вы платите цену во время выполнения ... что иногда подходит, но не подходит для дизайна. Кстати, если такой мысленный эксперимент вас интересует, вам могут понравиться аналоговые литералы hostilefork. com/29/08/2009/tweakinganalog-literals-humor   -  person HostileFork says dont trust SE    schedule 23.10.2011
comment
Я предположил, что вы хотите, чтобы оператор ›› был членом отображаемого объекта, но некоторые обсуждения указывают на другую интерпретацию: ›› быть членом потока. Что он?   -  person Bartosz Milewski    schedule 23.10.2011
comment
@BartoszMilewski Да, было указано, что ‹‹ и ›› могут быть функциями-членами, ЕСЛИ они находятся внутри потока ... и технически это относится к тому, что я изначально задал, по крайней мере, что касается названия вопроса. Но я попытался уточнить через комментарии и обновление вопроса (... непотоковые объекты, которые хотят сериализовать себя...)   -  person HostileFork says dont trust SE    schedule 23.10.2011


Ответы (5)


Вы получаете больше гибкости, отделяя определения ‹‹ и >> от отображаемого объекта.

Прежде всего, вы можете захотеть отображать тип, не относящийся к классу, например, перечисление или указатель. Предположим, у вас есть класс Foo, и вы хотите напечатать указатель на Foo. Вы не можете сделать это, добавив функцию-член в Foo. Или вы можете захотеть отобразить шаблонный объект, но только для определенного параметра шаблона. Например, вы можете отобразить вектор‹целое> в виде списка, разделенного запятыми, а вектор‹строка> – в виде столбца строки.

Другая причина может заключаться в том, что вам не разрешено или вы не хотите изменять существующий класс. Это хороший пример принципа открытости/закрытости в объектно-ориентированном проектировании, когда вы хотите, чтобы ваши классы были открыты для расширения, но закрыты для модификации. Конечно, ваш класс должен выставлять часть своей реализации, не нарушая инкапсуляцию, но это часто бывает (векторы выставляют свои элементы, комплекс выставляет Re и Im, строка выставляет c_str и т. д.).

Вы даже можете определить две разные перегрузки ‹‹ для одного и того же класса в разных модулях, если это имеет смысл.

person Bartosz Milewski    schedule 23.10.2011
comment
Хорошая мысль о развязке... и что, возможно, если вам нужно подружить свои операции сериализации, вы не предоставили достаточно API (который людям, использующим класс, может понадобиться по причинам, отличным от ввода-вывода...) - person HostileFork says dont trust SE; 25.10.2011

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

Обоснование:

Почему inserters и extractors не перегружаются как функции-члены?

Обычно правило перегрузки операторов таково:

Если бинарный оператор изменяет свой левый операнд, обычно полезно сделать его функцией-членом типа своего левого операнда (поскольку обычно требуется доступ к закрытым членам операндов).

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

person Alok Save    schedule 22.10.2011
comment
Я думаю, что вопрос был не в том, что сделано, а в том, почему библиотека не была разработана по-другому, чтобы поток был правым операндом и получал добро, такое как "C" >> "B" >> "A" >> cout; вывод ABC. - person UncleBens; 23.10.2011
comment
@UncleBens На самом деле я не хочу обратного хода. Единственное, что я искал, это как сделать потоковые операторы членами непотокового класса, который хочет иметь возможность сериализоваться. Я просто искал основную причину, по которой С++ не мог этого сделать - даже если бы кто-то был готов использовать другие операторы или приемы... интересно, позволит ли какая-нибудь лазейка сохранить его слева направо. - person HostileFork says dont trust SE; 23.10.2011
comment
@als Я понимаю, о чем вы говорите, и что реализация собственных классов потока означает, что вы можете выполнять свои операторы как члены класса... из потока. (Я думал только об объектах, которые не были потоками, но хотели быть сериализованными для них.) Я обновил вопрос, чтобы, надеюсь, лучше отразить любопытство языкового дизайна, которое я пытаюсь донести... - person HostileFork says dont trust SE; 23.10.2011
comment
@HostileFork: Ах, в таком случае этот ответ практически бесполезен для ответа на ваш настоящий вопрос, он просто отвечает на мета Q в заголовке. - person Alok Save; 24.10.2011

У меня был тот же вопрос раньше, и похоже, что вставки и экстракторы должны быть глобальными. Для меня это нормально; Я просто хочу избежать создания дополнительных «дружеских» функций в моем классе. Вот как я это делаю, т.е. предоставляю общедоступный интерфейс, который может вызывать "‹‹":

class Rock {
    private:
        int weight;
        int height;
        int width;
        int length;

    public:
        ostream& output(ostream &os) const {
            os << "w" <<  weight << "hi" << height << "w" <<  width << "leng" << length << endl;
            return os;
        }
    };

    ostream& operator<<(ostream &os, const Rock& rock)  {
        return rock.output(os);
    }
person Haoru HE    schedule 16.06.2013

Я пропустил какие-либо другие проблемы?

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

Обычный outfile << var1 << var2 << var3; — довольно «линейный» синтаксис. И, поскольку в обоих случаях мы читаем слева направо, имена будут в том же порядке, что и в файле.

Вы планируете сделать синтаксис нелинейным. Читателю-человеку придется пропустить вперед и вернуться, чтобы увидеть, что происходит. Это усложняет задачу. А вы идете еще дальше. Чтобы прочитать вашу последнюю строку, r >>= "Your rational number is " >>= cout; вам сначала нужно прочитать >>=, чтобы увидеть, что вам нужно перейти к последнему слову (или около того), прочитать «>>= cout», вернуться к началу строки , прочитать строку вперед и т.д. В отличие от перемещения вашего глаза (глаз) от одного токена к другому, когда мозг может конвейеризировать весь процесс.

У меня есть несколько лет опыта работы с языком с таким нелинейным синтаксисом. Теперь я изучаю возможность использования clang для «компиляции» C++ на этот язык. (Хотя по многим причинам.) Если это сработает, я буду намного счастливее.

Моя предпочтительная альтернатива — очень минимальная перегрузка оператора, которая вызывает только функцию-член. Если даже немного оптимизировать, он все равно исчезнет из созданного исполняемого файла.

Есть одно «очевидное» исключение из вышесказанного, когда у вас есть только одно чтение/запись в операторе, как в myobj >> cout;

person MaHuJa    schedule 22.10.2011
comment
Вы можете легко заставить поток (или что-то в этом роде) хранить данные ему значения, а затем распечатывать их в обратном порядке, чтобы избежать r >>= "Here's a value: " >>= cout. Конечно, я думаю, вам нужно что-то в начале цепочки, чтобы вызвать флеш. - person Chris Lutz; 23.10.2011
comment
Чтобы прочитать их в обратном порядке, вы должны назвать всю последовательность, которую собираетесь прочитать. Не только количество элементов, но и их типы. Это решение было бы довольно многословным и полностью противоречило бы цели этого. - person MaHuJa; 23.10.2011
comment
А зачем все указывать? Вы можете просто добавить special_object >>= stuff >>= to >>= print >>= cout и сделать так, чтобы special_object имел тип, сообщающий потоку о сбросе. Вы можете преобразовать все объекты в строки с помощью strstream и сохранить их все в vector<string> (или просто string и добавлять каждое новое добавление в начало), прежде чем читать special_object, который сбрасывает их все. - person Chris Lutz; 23.10.2011
comment
Несмотря на то, что пример кода касается вывода, я думал о вводе, когда писал его. Итак, вы правы, вы можете сделать это для вывода. Однако вопрос касается как ввода, так и вывода. - person MaHuJa; 23.10.2011
comment
Ввод будет намного сложнее. Я действительно хочу потратить четыре часа, пытаясь понять, как это сделать, но мне нужно написать статью, так что придется подождать. - person Chris Lutz; 23.10.2011
comment
Вы планируете сделать синтаксис нелинейным. ... Ну, я не совсем формально предлагал стандарт C++2X :) Я просто пытался добраться до суть того, почему C++ был принципиально неспособен иметь такой же хороший синтаксис для потоковой передачи без ущерба для какого-либо аспекта времени выполнения, и все же позволял потоковым операторам быть членами классов, которые хотели сериализовать. То, что я предлагаю, в основном заключается в том, что для этого не существует никакого трюка с оценкой только во время компиляции, кроме написания вещей справа налево, что функционально эквивалентно, но нотно более запутанно...! - person HostileFork says dont trust SE; 23.10.2011
comment
@MaHuJa Хорошее упоминание о минимальной перегрузке, которая вызывает метод, вместо того, чтобы возиться с friend. Я хотел сделать это, но моя основная проблема заключается в том, чтобы придумывать имена для этих методов и потенциально сбивать с толку читателей, привыкших к традиционной идиоме... - person HostileFork says dont trust SE; 23.10.2011
comment
@HostileFork У меня есть предложение: istream& foo::operator<< (istream&), которое отдельно разрешало бы синтаксис f << cin для ввода. (Не то, чтобы он хорошо справился с цепочкой.) Тогда глобальный istream& operator>> (istream& i, foo& f) { f << i; } позволил бы обычное использование. Тем не менее, ответ Бартоша дает некоторую полезную гибкость. - person MaHuJa; 13.11.2011

В C++ бинарные операторы обычно не являются членами класса. Согласно Языку программирования C++ Бьярна Страуструпа, оператор +, каноническое представление — это глобальная функция, которая сначала копирует свой левый операнд, затем использует += для него с правым операндом, а затем возвращает результат. Таким образом, глобальные потоковые операторы не являются чем-то из ряда вон выходящим. Как упоминал Элс, мы ожидаем, что операторы потока будут членами классов потоков, а не классов данных.

person Tom    schedule 24.10.2011