Простой выбор дизайна?

#c #overloading #friend

#c #перегрузка #друг

Вопрос:

Допустим, у меня есть класс с private data member n и public get_n() функцией. Например, при перегрузке оператора вывода я могу либо использовать get_n() , либо сделать его другом и использовать n . Есть ли «лучший» выбор? И если да, то почему? Или разница будет оптимизирована? Спасибо.

Комментарии:

1. Как вы думаете, почему функция get_n() вообще заметна по сравнению с самим выводом?

Ответ №1:

Используйте get_n, поскольку это не правильное использование friend. И если get_n это простой return n , компилятор, скорее всего, встроит его автоматически.

Комментарии:

1. -1 для друзей, не дружественных к ООП . Это не тот случай. Популярным примером являются классы с частными конструкторами, которые имеют заводской класс в качестве друзей. Это очень хороший объектно-ориентированный дизайн, который опирается на друзей. Более подробное объяснение можно найти в C FAQ lite .

2. @Space_C0wb0y: Это не то же самое, потому что элемент данных уже общедоступен.

3. @Space_C0wb0y: Спасибо за разъяснение ( 1 за то, что мне нравятся неудачные обобщения), и я знаю об использовании friend , но в случае с этим конкретным вопросом это действительно не решение. Я собираюсь отредактировать свой ответ, чтобы он был более понятным.

4. @DeadMG: Я согласен, но в ответе говорится об этом в очень общем виде (говоря, что они не подходят для ООП , вместо они здесь неуместны ), поэтому я чувствовал себя обязанным противоречить.

Ответ №2:

Я отвечу на ваш вопрос вопросом:

  • Почему вы создали public get_n() в первую очередь?

Комментарии:

1. Это был фиктивный пример. У меня мог бы быть простой контейнер с закрытым элементом sz и общедоступной функцией size() const.

2. @Garp: И это был риторический вопрос.

Ответ №3:

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

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

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

Это все еще оставляет небольшой вопрос о том, как вы должны что-то делать. Это (конечно) приводит к большему количеству вопросов. В частности, что за вещь n представляет? Я понимаю, что вы, вероятно, приводите гипотетический пример, но хороший дизайн (в данном случае) зависит от знания немного большего о том, что n такое и как это используется, а также о типе, который n является элементом, и как он также предназначен для использования.

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

 class whatever { 
    int n;

    friend std::ostream amp;operator<<(std::ostream amp;os, whatever const amp;w) { 
        return os << w.n;
    }
};
  

Простой, понятный и эффективный.

Однако, если n является членом чего-либо, что вы ожидаете использовать (или быть использованным) в качестве базового класса, то обычно вы хотите использовать «виртуальную виртуальную» функцию:

 class whatever { 
    int n;

    virtual std::ostream amp;write(std::ostream amp;os) { 
        return os << n;
    }    

    friend std::ostream amp;operator<<(std::ostream amp;os, whatever const amp;w) { 
        return w.write(os);
    }
};
  

Обратите внимание, однако, что это предполагает, что вы заинтересованы в написании всего объекта, и просто случается, что, по крайней мере, в текущей реализации, это означает запись значения n .

Что касается того, почему вы должны делать что-то таким образом, есть несколько простых принципов, которым, я думаю, следует следовать:

  1. Либо сделайте что-то действительно закрытым, либо сделайте это общедоступным. Закрытый элемент с public get_n (и, как правило, также public set_n ) может потребоваться для JavaBeans (для одного примера), но это все еще действительно плохая идея и демонстрирует грубое непонимание объектной ориентации или инкапсуляции, не говоря уже о создании совершенно уродливого кода.
  2. Скажи, не спрашивай. Общедоступность get_n часто означает, что в конечном итоге вы получаете клиентский код, который выполняет цикл чтения / изменения / записи, при этом объект действует как обычный контейнер данных. Обычно предпочтительнее преобразовать это в единственную операцию, в которой клиентский код описывает желаемый результат, а сам объект выполняет чтение / модификацию / запись для достижения этого результата.
  3. Минимизировать интерфейс. Вы должны стремиться к тому, чтобы каждый объект имел как можно меньший интерфейс, не причиняя излишней боли пользователям. Устранение общедоступной функции типа get_n почти всегда хорошо само по себе, независимо от того, подходит ли она для инкапсуляции.

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

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

Если объект поддерживает ввод и / или вывод, то ввод и вывод являются частями интерфейса этого объекта, и все, что реализует этот интерфейс, является частью объекта. Единственная другая возможность заключается в том, что тип объекта не поддерживает ввод и / или вывод.

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

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

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

Комментарии:

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

Ответ №4:

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

Комментарии:

1. В нем уже есть toString метод — оператор вставки потока.

2. @DeadMG: Оператор вставки потока не является методом (что для меня означает то же самое, что функция-член ). Это всегда должна быть свободная функция (для пользовательских типов).

Ответ №5:

Может ли оператор быть реализован без использования friend ? Да — не используйте friend . Нет- make friend .