Перегрузка вставки потока без нарушения сокрытия информации?

Я использую yaml-cpp для проекта. Я хочу перегрузить операторы << и >> для некоторых классов, но у меня возникла проблема с тем, как это сделать "правильно". Возьмем, к примеру, класс Note. Это довольно скучно:

class Note {
  public:
    // constructors
    Note( void );
    ~Note( void );

    // public accessor methods
    void            number( const unsigned long& number ) { _number = number; }
    unsigned long   number( void ) const                  { return _number; }
    void            author( const unsigned long& author ) { _author = author; }
    unsigned long   author( void ) const                  { return _author; }
    void            subject( const std::string& subject ) { _subject = subject; }
    std::string     subject( void ) const                 { return _subject; }
    void            body( const std::string& body )       { _body = body; }
    std::string     body( void ) const                    { return _body; }

  private:
    unsigned long   _number;
    unsigned long   _author;
    std::string     _subject;
    std::string     _body;
};

Оператор << — это легкий соус. В .h:

YAML::Emitter& operator << ( YAML::Emitter& out, const Note& v );

И в .cpp:

YAML::Emitter& operator << ( YAML::Emitter& out, const Note& v ) {
  out << v.number() << v.author() << v.subject() << v.body();
  return out;
}

Нет пота. Затем я объявляю оператор >>. В .h:

void operator >> ( const YAML::Node& node, Note& note );

Но в .cpp я получаю:

void operator >> ( const YAML::Node& node, Note& note ) {
  node[0] >> ?
  node[1] >> ?
  node[2] >> ?
  node[3] >> ?
  return;
}

Если я напишу что-то вроде node[0] >> v._number;, то мне нужно будет изменить квалификатор CV, чтобы сделать все поля Note public (что опровергает все, чему меня учили (профессора, книги и опыт)) о сокрытии данных.

Я чувствую, что делать node[0] >> temp0; v.number( temp0 ); повсюду не только утомительно, подвержено ошибкам и уродливо, но и довольно расточительно (что с дополнительными копиями).

Потом я сообразил: я попытался переместить эти два оператора в сам класс Note и объявить их как friends, но компилятору (GCC 4.4) это не понравилось:

src/note.h:44: error: 'YAML::Emitter& Note::operator‹‹(YAML::Emitter&, const Note&)' должен принимать ровно один аргумент
src/note.h:45: error: ' void Note::operator>>(const YAML::Node&, Note&)' должен принимать ровно один аргумент

Вопрос: как "правильно" перегрузить оператор >> для класса

  1. Не нарушая принцип сокрытия информации?
  2. Без лишнего копирования?

person Chris Tonkinson    schedule 07.06.2010    source источник
comment
Ошибки при перемещении operator<< в качестве функции-члена говорят вам, что когда вы переопределяете оператор как функцию-член, левый операнд должен относиться к этому типу класса, а правая часть является единственным аргументом. оператора. Вы не можете переопределить оператор, который принимает YAML::Emitter в качестве первого аргумента в качестве члена класса вне класса YAML::Emitter.   -  person David Rodríguez - dribeas    schedule 07.06.2010
comment
Рассмотрите возможность возврата строк по const ссылке.   -  person sbi    schedule 07.06.2010


Ответы (5)


Типичный способ сделать это, не нарушая инкапсуляцию, состоит в том, чтобы сделать функцию operator>> дружественной. Должно быть, возникла синтаксическая проблема с вашим объявлением оператора-друга (из сообщения об ошибке неясно, что именно). Я не использую YAML, но из вашего вопроса следует следующее:

class Note{
    ...
    friend void operator >> ( const YAML::Node& node, Note& note );
    ....
 };
 void operator >> ( const YAML::Node& node, Note& note ){
    node[0] >> note._number;
    node[1] >> note._author;
    node[2] >> note._subject;
    node[3] >> note._body;
 }

Функция друга имеет те же права доступа к закрытым членам, что и функция-член.

В качестве альтернативы вы можете объявить сеттеры для всех данных-членов, но метод дружественной функции будет чище.

person academicRobot    schedule 07.06.2010
comment
Я согласен, что все эти сеттеры являются несовершенством, но он уже получил их все, они просто берут менее чем полезный тип аргумента для этого варианта использования, поэтому добавление перегрузок с действительно полезным типом аргумента вряд ли добавляет что-либо. проблема. - person Alex Martelli; 07.06.2010
comment
@Alex Martelli Возможно, но опять же, объявить функцию друга (и ее чище, ИМХО) тоже не проблема. - person academicRobot; 07.06.2010
comment
Я не уверен, в чем проблема, но я попробовал это снова сегодня, и это сработало; Я думаю, вы правы, должно быть, проблема с синтаксисом. - person Chris Tonkinson; 07.06.2010

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

class Note {
public:
    void read(const YAML::Node& node)
    {
        node >> ...;
    }
};

а затем пусть operator>> просто переадресует вызов:

const YAML::Node &operator >> ( const YAML::Node& node, Note& note ) {
    note.read(node);
    return node;
}
person R Samuel Klatchko    schedule 07.06.2010
comment
Я бы не стал этого делать. У класса уже есть интерфейс записи, и добавление этого метода создает зависимость от Note к YAML, которая на самом деле не нужна (вы больше не можете использовать Note в контексте, где нет YAML). - person David Rodríguez - dribeas; 07.06.2010

Вы определяете дополнительные методы установки в Note, такие как

void number(YAML::Immitter& e) { e>>_number; }

и т. д., и затем вы определяете синтаксический сахар >> как

void operator >> ( YAML::Immitter& e, Note& note ) {
  note.number(e);
  note.author(e);
  note.subject(e);
  note.body(e);
}

Я не знаком с пространством имен YAML, которое вы используете (я знаю yaml, но я никогда не работал с ним на C++), но примерно так вы поступили бы с обычными потоками (кроме возвращаемых типов void;-), и я уверен, что его можно легко адаптировать к вашим конкретным потребностям.

person Alex Martelli    schedule 07.06.2010

В вашем классе уже есть методы установки. Просто используйте временные файлы для чтения значений и используйте методы установки для настройки объекта:

void operator >> ( const YAML::Emitter& node, Note& note ) {
  unsigned long number;
  unsigned long author;
  // ...
  node[0] >> number;
  node[1] >> author;
  // ... everything properly read, edit the node:
  node.number(number);
  node.author(author);
  // ...
  return;

}

Некоторые другие комментарии: класс, который имеет сеттеры/геттеры для всех атрибутов, вряд ли инкапсулирован. Вы предоставляете пользователям тот же уровень доступа, как если бы ваши поля были на самом деле общедоступными (с единственным преимуществом, что вы можете добавить проверку позже, но все равно инкапсуляция слаба).

В решениях, которые предлагают добавить метод-член, который принимает узел YAML, это добавит дополнительную зависимость для всех пользователей вашего класса. Хотя вы можете использовать предварительные объявления, чтобы не заставлять их включать заголовки YAML, вы не сможете извлечь библиотеку с вашим Note для использования в другом проекте, который не использует YAML легко.

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

person David Rodríguez - dribeas    schedule 07.06.2010
comment
Я полностью согласен с комментарием «вряд ли инкапсулированный». В нынешнем виде все члены данных в любом случае являются общедоступными. Осталось не так много принципа сокрытия информации, который можно было бы нарушить. - person CB Bailey; 07.06.2010
comment
Все члены данных в классе могут быть эффективно удалены/переименованы/вычислены на лету позже без каких-либо изменений в пользовательском интерфейсе. С геттерами/сеттерами информация о реализации полностью скрыта. Это единственный способ работы с классом, который является общедоступным. - person Alsk; 07.06.2010
comment
@Alsk, с теоретической точки зрения я согласен, но по моему опыту, во всех проектах, которые я видел, этот шаблон единственным реальным преимуществом перед простыми общедоступными атрибутами была возможность добавления проверки инвариантов в класс. В любом случае, это в основном комментарий к инкапсуляции, дело в том, что с общедоступными сеттерами, которые позволяют вам полностью изменять состояние, вам больше ничего не нужно, вы можете строить поверх этих сеттеров. А построение функциональности поверх существующего общедоступного интерфейса снижает связанность. - person David Rodríguez - dribeas; 07.06.2010

Ну, вот идея, которую вы могли бы рассмотреть. Вы говорите, что проблема с функцией ‹‹, которая не является другом и не членом, заключается в том, что она включает в себя множество объявлений tmp. Рассматривали ли вы возможность инкапсулировать концепцию и построить вокруг нее повторно используемый компонент? Использование может выглядеть примерно так:


inputter& operator >> (inputter& in, my_type & obj)
{
  input_helper<my_type> helper(obj);

  in >> helper.setter(&my_type::number);
  in >> helper.setter(&my_type::subject);
  // etc
}

Ответственность input_helper состоит в том, чтобы просто предоставить функцию шаблона setter(), которая возвращает объект, который просто считывает значение и вызывает с ним сеттер, создавая необходимую временную переменную. Код, подобный этому, потребует некоторого близкого знакомства с шаблонами, но не будет особенно сложным. Я не могу сейчас соображать полностью — может быть, простудился — или я, вероятно, смог бы просто напечатать это. Может быть, что-то вроде этого:


template < typename T >
struct input_helper
{
  input_helper(T & t) : obj(t) {}

  template < typename V >
  struct streamer
  {
    streamer(T & t, void (T::*f)(V const&)) : obj(t), fun(f) {}

    template < typename Stream >
    Stream& read_from(Stream & str) const // yeah, that's right...const; you'll be using a temporary.
    {
      V v;
      str >> v;
      obj.(*fun)(v);
      return str;
    }

  private: // you know the drill...
  }

  template < typename V >
  streamer setter(void (T::*fun)(V const&))
  {
    return streamer(obj, fun);
  }
private:
  T & obj;
};
// etc...  operator >> (blah blah) { return setter.read_from(stream); }

Конечно, в этом есть всевозможные ошибки, но это должно дать вам представление. Также потребуется больше работы, чтобы обобщить.

person Edward Strange    schedule 07.06.2010
comment
Сначала я прочитал это как helper.setter( &my_type::number_ ), что выдаст ошибку, поскольку number_ является частным. Однако использование &my_type::number также не работает, так как есть две функции number. Вам понадобится какая-то мерзость, такая как ( unsigned long(mytype::*)() )( &my_type::number), чтобы устранить неоднозначность ... хуже, чем использование временных значений ИМХО. - person Michael Anderson; 07.06.2010
comment
Я считаю, что тип параметра функции устраняет неоднозначность. Хотя я ничего из этого не тестировал. Даже если это не так, есть много способов обойти то, о чем вы говорите. - person Edward Strange; 07.06.2010
comment
Я протестировал этот конкретный аспект кода, и он действительно отлично компилируется. На какую версию номера ссылается адрес, определяется типом, которому он назначается (параметр функции). - person Edward Strange; 07.06.2010