Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > C/C++: Общие вопросы > Абстрактный класс и operator <<


Автор: T0ohtik 19.7.2008, 01:12
Доброй ночи, товарищи! Возникла задача написать базовый, абстрактный класс. И перегрузить опретацию <<. Получается, что для достижения этой целин надо написать виртуальную, дружественную, нулевую функцию. Возможно ли такое или я что то нагородил?

Автор: vinter 19.7.2008, 08:57
делаешь виртуальный\чисто виртуальный оператор в базовом классе, например +=, потом переопределяешь его в потомке. И делаешь внешнюю ф-ию например +, и все..

Автор: Annihilator 19.7.2008, 09:49
Попробуй так
Код

class ClassA
{
    virtual ostream& out(ostream& os) const = 0;
    friend ostream& operator<<(ostream& os, const ClassA& a) {
        return a.out(os);
    }
};

class ClassB1 : public ClassA
{
    ostream& out(ostream& os) const {
        os << "ClassB1::out(ostream& os)";
        return os;
    }
};

class ClassB2 : public ClassA
{
    ostream& out(ostream& os) const {
        os << "ClassB2::out(ostream& os)";
        return os;
    }
};

Автор: Torsten 19.7.2008, 11:19
У меня почти такой же вариант, как и у Annihilator, только оператор << не дружественная функция.

Код

class A
{
public:
    virtual ~A() {}
    virtual std::ostream & Print(std::ostream & strm) const = 0;
};

std::ostream & operator<<(std::ostream & os, const ClassA & a)
{
       return a.Print();
}

Автор: Ulysses4j 19.7.2008, 19:00
Я за версию Annihilator, хотя версия Torsten соответствует тому, что у Мейерса. Мотивация такая: out не предназначена для внешних пользователей — клиенты этой иерархии классов должны использовать operator<< — но есть одно исключение, сам operator<<: это значит, надо делать out закрытой, а operator<< френдить.

Но вместо out лучше print, да (со строчной буквы!).

Автор: vinter 19.7.2008, 21:34
Цитата(Ulysses4j @  19.7.2008,  20:00 Найти цитируемый пост)
хотя версия Torsten соответствует тому, что у Мейерса

Насколько я помню Майерс за инкапсуляцию, а френды ее нарушают. Вообще френды надо юзать при крайней необходимости, я считаю.

Автор: Ulysses4j 19.7.2008, 22:16
Цитата(vinter @  19.7.2008,  22:34 Найти цитируемый пост)
 Майерс за инкапсуляцию

 smile Понимаете, все мы за инкапсуляцию: я, вы, Мейерс — но френдов время от времени используют даже “великие”. Просто Мейерс реализовал так и почему-то никак своего решения не объяснил (это, если вдруг интересно, More Effective, 25). Я пояснил, почему на мой взгляд открыть print в данном случае плохо: это, если непонятно, еще более очевидным образом нарушает инкапсуляцию. 

Есть еще такая мотивировка. Изначально operator<< хочется сделать членом класс (так и Мейерс, кстати, начинает рассказывать, но это очевидно и из общих соображений: функция вывода в поток должна иметь доступ к представлению, чтобы его непосредственно и выводить). Но если поступить так, то не получится реализовать привычную идиому os << foo; (придется задом наперед). Вот и идут на этот финт с print. Но если вы изначально хотели предоставить operator<< доступ к реализации, то чего уж бояться делать его френдом: ведь член это более близкая связь чем френд.

С print есть еще проблема: пользователь класса будет смотреть и видеть две функции одного назначения: print и operator<<. Это ненужное раздувание интерфейса, умножение сущностей без необходимости (привет Оккаму). В случае с закрытой print для пользователя есть только одна функция выполняющая одну работу. Все предельно ясно.

И все-таки меня терзают смутные сомнения, что я где-то читал вариант с закрытой print. Что за память...

Автор: UnrealMan 19.7.2008, 23:15
Цитата(Ulysses4j @  19.7.2008,  22:16 Найти цитируемый пост)
С print есть еще проблема: пользователь класса будет смотреть и видеть две функции одного назначения: print и operator<<. Это ненужное раздувание интерфейса, умножение сущностей без необходимости (привет Оккаму). В случае с закрытой print для пользователя есть только одна функция выполняющая одну работу. Все предельно ясно.

Однако, тот, кто наследуется, должен знать о существовании print у базового класса. Т.е. print должна быть задокументирована.

Автор: Ulysses4j 19.7.2008, 23:57
Да, дельное замечание. Под клиентами я, разумеется, понимал классы, непосредственно использующие объекты рассматриваемого класса (с “виртуальной” функцией вывода в поток) и его наследников. Все-таки по умолчанию я предполагаю, что человек не наследуется от классов чужой библиотеки (наследование реализации многажды поруганная стратегия... впрочем, как и френдизм smile). А в рамках одной библиотеки человек/команда проектирует иерархию классов со всеми виртуальностями и должны, конечно, быть осведомлены об используемом механизме.

Автор: Torsten 20.7.2008, 00:45
Цитата(Ulysses4j @  19.7.2008,  22:16 Найти цитируемый пост)
Я пояснил, почему на мой взгляд открыть print в данном случае плохо: это, если непонятно, еще более очевидным образом нарушает инкапсуляцию. 

И чем же она нарушает инкапсуляцию ? Тем что выводит свои данные в поток ? Ее обычно и создают, чтобы выводить информацию, а не скрывать ее.


Цитата(Ulysses4j @  19.7.2008,  22:16 Найти цитируемый пост)
С print есть еще проблема: пользователь класса будет смотреть и видеть две функции одного назначения: print и operator<<. Это ненужное раздувание интерфейса, умножение сущностей без необходимости (привет Оккаму). В случае с закрытой print для пользователя есть только одна функция выполняющая одну работу. Все предельно ясно.

Можно создать класс printable, у которого будет переопределен оператор << и вызывается виртуальная функция print. Унаследованный класс должен будет переопредилить функцию print, и в тоже самое время ему не нужно будет определять оператор <<, т.к. он реализован для класса printable. Когда пользователь будет смотреть архитектуру класса он не будет видеть лишнего и сам интерфейс не будет раздуватся, т.к. достаточно будет в проивзодных классах определить только 1 функцию.


Цитата(Ulysses4j @  19.7.2008,  22:16 Найти цитируемый пост)
Есть еще такая мотивировка. Изначально operator<< хочется сделать членом класс (так и Мейерс, кстати, начинает рассказывать, но это очевидно и из общих соображений: функция вывода в поток должна иметь доступ к представлению, чтобы его непосредственно и выводить). Но если поступить так, то не получится реализовать привычную идиому os << foo; (придется задом наперед). Вот и идут на этот финт с print. Но если вы изначально хотели предоставить operator<< доступ к реализации, то чего уж бояться делать его френдом: ведь член это более близкая связь чем френд.

Это мнение ошибчно. Для того, чтобы это понять нужно понять "принцип интерфейса". 
В случае ostream & operator <<(ostream & strm, const Foo & foo) { /*код для вывода в поток */} в соотвествии с принципом интерфейса.
Так как operator << упоминает Foo - он является логической часть Foo. 
operator << упоминает ostream, поэтому operator << зависит от ostream.
operator << является логической частью Foo и зависит от ostream, следовательно Foo зависит от ostream. 

Таким образом реализация посредством френд оператора <<, ничем не лучше функции Print. А Print как я уже и писал выше имеет преимущество перед френд оператором в том, что в поток корректно выводтся все производные от X классы, даже при передаче оператору << ссылки производных классов.

Цитата
Но вместо out лучше print, да (со строчной буквы!).

Это вопрос стиля и правил разработки принемаемых перед созданием проекта. 

Автор: Ulysses4j 20.7.2008, 01:20
Цитата(Torsten @  20.7.2008,  01:45 Найти цитируемый пост)
И чем же она нарушает инкапсуляцию ? Тем что выводит свои данные в поток ? Ее обычно и создают, чтобы выводить информацию, а не скрывать ее.

Нет, не тем что выводит данные в поток, а тем, что создавалась она только для использования в operator<<, а а выложив ее в public, ей смогут пользоваться клиенты класса. Ее создают не для того, чтобы выводить данные в поток — видимо, вы позабыли, что print создавалась, чтобы задействовать механизм виртуальности, это деталь реализации operator<<, роль которого как раз вывод в поток — с точки зрения клиента.

Цитата(Torsten @  20.7.2008,  01:45 Найти цитируемый пост)
Print как я уже и писал выше имеет преимущество перед френд оператором в том, что в поток корректно выводятся все производные от X классы, даже при передаче оператору << ссылки производных классов.

Вы не понимаете: это не преимущество print, а ее назначение: обеспечивать виртуальность. Если бы она этого не делала, обошлись бы без нее. Ну, или без operator<<. Если уж решили его сделать, то он должен быть единственным средством вывода в поток.

“Принцип интерфейса” не осилил  smile

Еще один аргумент закрытия print это совет из Стандартов кодирования Саттера&Александреску, повторяемый у Саттера в More Exceprtional и Дьюхерста в Готчах (или если по хронологии, то наоборот, кажется): делайте виртуальные функции закрытыми (идиома non-virtual interface, NVI). В данном случае невиртуальный интерфейс это operator<<, а спрятанная по канонам Саттера-Александреску-Дьюхерста виртуальность это print.

Автор: Torsten 20.7.2008, 21:31
Цитата(Ulysses4j @  20.7.2008,  01:20 Найти цитируемый пост)
Нет, не тем что выводит данные в поток, а тем, что создавалась она только для использования в operator<<, а а выложив ее в public, ей смогут пользоваться клиенты класса.


Ну и что дальше ? Ну могут они данные вывести, но данные изменять не могут и кроме того они понятие не имеют о том что выводится, так что здесь все нормально.


Цитата(Ulysses4j @  20.7.2008,  01:20 Найти цитируемый пост)
Вы не понимаете: это не преимущество print, а ее назначение: обеспечивать виртуальность. Если бы она этого не делала, обошлись бы без нее. Ну, или без operator<<. Если уж решили его сделать, то он должен быть единственным средством вывода в поток.

Ну если вы не понимаете, тогда я обьясню на коде.
Код

class Message
{
public:
      vritual ~Message() {}
...
};

ostream operator << (ostream & strm, const Message & Msg) { ... }

class SystemMessage : public Message {...};
ostream operator << (ostream & strm, const SystemMessage & Msg) { ... }

class UserMessage : public Message {...};
ostream operator << (ostream & strm, const UserMessage & Msg) { ... }

Message * pSystem = new SystemMessage;
Message * pUser = new UserMessage;

cout << pSystem << pUser ;


Будет использоватся оператор вывода для абстрактного класса Message.
Если реализовывать с помощью функции Print, то во-первых не нужно будет писать для каждого класса оператор <<, а во-вторых самое главное, всегда будут выводится реальные данные, то есть SystemMessage и UserMessage, т.к. функция Print будет вызвана у них.

Автор: Ulysses4j 20.7.2008, 21:47
Цитата(Torsten @  20.7.2008,  22:31 Найти цитируемый пост)
Ну и что дальше ? Ну могут они данные вывести, но данные изменять не могут и кроме того они понятие не имеют о том что выводится, так что здесь все нормально.

Так вы полагаете что инкапсуляция означает невозможность изменить данные? Ну, спешу вас огорчить, это намного более общее понятие: инкапсуляция подразумевает сокрытие реализации класса и его артефактов (в C++ — в частности, тесно связанных с классом свободных функций). Я надеюсь, вы когда-нибудь использовали private-методы. Это тоже инкапсуляция. Надеюсь, когда-нибудь использовали private-константы внутри класса: их можно было бы открыть, потому что их изменить нельзя (хаки с const-кастом не берем, можно рассмотреть другие ОО-языки, в которых подобных cast-ов нет). Однако, закрытые константы используются. Это тоже инкапсуляция.

И скрытие деталей того, как виртуализирована operator<<, то есть закрытие print, это тоже инкапсуляция.

Цитата(Torsten @  20.7.2008,  22:31 Найти цитируемый пост)
Ну если вы не понимаете, тогда я объясню на коде.

О! Вы попытались объяснить мне идиому виртуальной свободной функции, как она описана у Мейерса (см. мою ссылку выше)! smile  Чтоб я б я без вас делал...

Автор: T0ohtik 20.7.2008, 22:37
Извините, что долго отсутствовал. Спасибо всем кто ответил!

Автор: UnrealMan 21.7.2008, 12:35
Цитата(Ulysses4j @  19.7.2008,  23:57 Найти цитируемый пост)
наследование реализации многажды поруганная стратегия... 

Те, кто её ругают, идут лесом. Делая Print закрытой, мы лишаем пользователя возможности осуществлять невиртуальный вызов для базового подобъекта.

Автор: Ulysses4j 21.7.2008, 13:18
Ну, если считаете необходимым вызывать в потомках, делайте protected. Я же говорю: видимость должна отражать назначение.
Цитата(UnrealMan @  21.7.2008,  13:35 Найти цитируемый пост)
Те, кто её ругают, идут лесом.

Ну-ну, бедные-бедные ребята, эти авторы красных книжек с автором серии во главе: ушли лесом...

Автор: UnrealMan 21.7.2008, 14:01
Цитата(Ulysses4j @  21.7.2008,  13:18 Найти цитируемый пост)
Ну-ну, бедные-бедные ребята, эти авторы красных книжек с автором серии во главе: ушли лесом... 

Прежде чем читать умные книжки, надо выучить букварь. А то получается "гляжу в книгу, а вижу фигу". У некоторых людей просто мания какая-то путать частное с общим.

Автор: Ulysses4j 21.7.2008, 14:12
А, то есть вы не осили букварь и теперь кругом видите фиги?  smile  Сочувствую... 

Автор: UnrealMan 21.7.2008, 14:32
Цитата(Ulysses4j @ 21.7.2008,  14:12)
А, то есть вы не осили букварь и теперь кругом видите фиги?  smile  Сочувствую...

Неумение внимательно читать книги, равно как и отсутствие самокритики, - это твоя проблема, а мне плевать на чужие проблемы :p

Автор: Ulysses4j 21.7.2008, 14:41
Ой, а можно пример моего невнимательного чтения книг? (Самокритику опустим.)

Автор: UnrealMan 21.7.2008, 15:44
Цитата(Ulysses4j @  19.7.2008,  23:57 Найти цитируемый пост)
Все-таки по умолчанию я предполагаю, что человек не наследуется от классов чужой библиотеки 

Контрпример можно найти прямо под носом: std::basic_ostream из стандартной библиотеки C++. Если пользователь захочет реализовать вывод  информации на некоторое устройство через потоки, то он может создать новый потоковый класс, унаследовав его от std::basic_ostream (подобно тому, как это делается для basic_ofstream и basic_ostringstream). Данный базовый класс - вовсе никакой не абстрактный, и наследование реализации здесь имеет место быть. Никаких противоречий с рассуждениями, приводимыми в прочитанных мной книгах классиков, здесь нет. То, что является потоком вывода, должно быть унаследовано от std::basic_ostream.

Автор: Ulysses4j 21.7.2008, 16:14
Я же не говорил, что нужно запретить наследование реализации. В некоторых случаях избежать ее нельзя или трудно, это верно. Пример хороший, он демонстрирует слабость стандартной библиотеки, которая вынуждает вас наследовать реализацию, когда вы хотите наследовать интерфейс. Стандартная библиотека C++ не идеальна, как и не идеален C++, в котором понятие интерфейса не имеет воплощения в синтаксисе языка. Учтите. кстати, что все-таки есть определенная разница между стандартной библиотекой языка и сторонней библиотекой, к которой я аппелировал в посте. Классы потоков зашиты во многих интерфейсах именно потому что они входят в состав стандартных средств, в этой ситуации наследование от них для того, чтобы быть повторно использованным, а не для того, чтобы повторно использовать (© не помню кто), может быть вполне оправдано.

Еще раз прочитайте то, что вы отцитировали: я же говорю «по умолчанию». Если вы можете обосновать необходимость использования чего-то в конкретном случае, это замечательно, пользуйтесь. В том посте, да и в следующем вашем комментарии про использовании print в наследниках, я вас поддержал. Собственно, я не понимаю, в чем педмет ваших претензий ко мне.

Автор: UnrealMan 21.7.2008, 16:50
Цитата(Ulysses4j @  21.7.2008,  16:14 Найти цитируемый пост)
В некоторых случаях избежать ее нельзя или трудно, это верно. 

"Некоторых" - это вроде как немногочисленных? Откуда такая статистика?

Цитата(Ulysses4j @  21.7.2008,  16:14 Найти цитируемый пост)
Пример хороший, он демонстрирует слабость стандартной библиотеки, которая вынуждает вас наследовать реализацию, когда вы хотите наследовать интерфейс. 

О да, мы просто мечтаем унаследовать голый интерфейс, а всё форматирование, контроль за состоянием и т.д. реализовать с нуля ручками smile 

Цитата(Ulysses4j @  21.7.2008,  16:14 Найти цитируемый пост)
Учтите. кстати, что все-таки есть определенная разница между стандартной библиотекой языка и сторонней библиотекой, к которой я аппелировал в посте. Классы потоков зашиты во многих интерфейсах именно потому что они входят в состав стандартных средств, в этой ситуации наследование от них для того, чтобы быть повторно использованным, а не для того, чтобы повторно использовать (© не помню кто), может быть вполне оправдано.

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

Автор: Ulysses4j 21.7.2008, 17:01
Цитата(UnrealMan @  21.7.2008,  17:50 Найти цитируемый пост)
"Некоторых" - это вроде как немногочисленных? Откуда такая статистика?

От друзей, зарабатывающих на жизнь программированием, и из книжек.

Цитата(UnrealMan @  21.7.2008,  17:50 Найти цитируемый пост)
О да, мы просто мечтаем унаследовать голый интерфейс, а всё форматирование, контроль за состоянием и т.д. реализовать с нуля ручками  

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

Я так понял, что вы хотите наследоваться от basic_ostream в вашем примере, чтобы ваш класс мог использоваться там, где уже используется ostream, то есть — много где(здесь возникает аргумент о том, что стандартная библиотека отличается от обыкновенной — своей распространенностью; надеюсь, больше повторять не придется). Если вы хотели использовать какие-то protected-члены ostream, приведите, пожалуйста, пример, какие. Большая часть классов стандартной библиотеки не предназначена для наследования реализации, там никаких особо продуманных protected-интерфейсов нет, так что я с интересом бы послушал, что вам там приглянулось...

Автор: NDQuattro 21.7.2008, 17:10
Люди, скажите плз может ли такому определению функции 
Код

int func(double x = 0, double y); 
  соответствовать вызов 
Код

func(5.98)



Автор: JackYF 21.7.2008, 17:19
NDQuattro, создавай отдельную тему!

Автор: NDQuattro 21.7.2008, 17:20
ок, просто ради такой мелочи форум мусорить не хотелось что то))

Автор: Annihilator 21.7.2008, 17:44
Цитата(NDQuattro @  21.7.2008,  21:20 Найти цитируемый пост)
ок, просто ради такой мелочи форум мусорить не хотелось что то))

Какие проблемы? Читай книги и не надо будет мусорить! (сорри  smile )

Автор: UnrealMan 21.7.2008, 17:52
Цитата(Ulysses4j @  21.7.2008,  17:01 Найти цитируемый пост)
От друзей, зарабатывающих на жизнь программированием

Этих друзей, видимо, сотни, каждый из которых программирует в своей уникальной предметной области и притом лично сообщил тебе статистические данные своей работы? Что-то с трудом верится. А иначе это получается не очень репрезентативно.

Цитата(Ulysses4j @  21.7.2008,  17:01 Найти цитируемый пост)
и из книжек

Это уже обнадёживает, правда не хватает немного конкретики (автор и название хотя бы одной из этих книг, глава, пункт, абзац).

Цитата(Ulysses4j @  21.7.2008,  17:01 Найти цитируемый пост)
Я так понял, что вы хотите наследоваться от basic_ostream в вашем примере, чтобы ваш класс мог использоваться там, где уже используется ostream, то есть — много где

По-моему, совершенно очевидно, зачем нужно это наследование. Во-первых, basic_ostream берёт на себя много рутинной работы, во-вторых, классу, переопределяющему операцию вывода <<, нет необходимости различать, в какой именно поток будут выводить информацию, будь это cout, объект типа ofstream, объект типа ostringstream или объект типа MyOStream - с каждым видом потока можно работать единообразно.

Цитата(Ulysses4j @  21.7.2008,  17:01 Найти цитируемый пост)
здесь возникает аргумент о том, что стандартная библиотека отличается от обыкновенной — своей распространенностью; надеюсь, больше повторять не придется

Я не понимаю, каким боком распространённость в данном плане на что-либо влияет.

Автор: Ulysses4j 21.7.2008, 20:04
Так, похоже, из того, что я писал выше, вы осили процентов 20. Я считаю такой кпд слишком низким для нашей увлекательной беседы, так что засим разрешите откланиться.

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)