Подтвердить что ты не робот

Функции, отличные от других, не связанные с частями

Herb Sutter сказал, что наиболее объектно-ориентированный способ написания методов в С++ использует не-членные функции, отличные от друга. Должно ли это означать, что я должен принимать частные методы и превращать их в не-членские функции, отличные от друга? Любые переменные-члены, которым могут понадобиться эти методы, могут быть переданы как параметры.

Пример (до):

class Number {
 public:
  Number( int nNumber ) : m_nNumber( nNumber ) {}
  int CalculateDifference( int nNumber ) { return minus( nNumber ); }
 private:
  int minus( int nNumber ) { return m_nNumber - nNumber; }
  int m_nNumber;
};

Пример (после):

int minus( int nLhsNumber, int nRhsNumber ) { return nLhsNumber - nRhsNumber; }
class Number {
 public:
  Number( int nNumber ) : m_nNumber( nNumber ) {}
  int CalculateDifference( int nNumber ) { return minus( m_nNumber, nNumber ); }
 private:
  int m_nNumber;
};

Я на правильном пути? Следует ли перенести все частные методы в функции, отличные от других? Какими должны быть правила, которые говорили бы вам иначе?

4b9b3361

Ответ 1

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

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

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

class Number { // small simple interface: accessor to constant data, constructor
public:
  explicit Number( int nNumber ) : m_nNumber( nNumber ) {}
  int value() const { return m_nNumber; }
private:
  int m_nNumber;
};
Number operator+( Number const & lhs, Number const & rhs ) // Add addition to the interface
{
   return Number( lhs.value() + rhs.value() );
}
Number operator-( Number const & lhs, Number const & rhs ) // Add subtraction to the interface
{
   return Number( lhs.value() - rhs.value() );
}

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

Жесткая часть (не в упрощенном примере выше) определяет, какой минимальный интерфейс вы должны предоставить. Статья (GotW # 84), ссылка на предыдущий вопрос здесь - отличный пример. Если вы прочтете его подробно, вы обнаружите, что можете значительно уменьшить количество методов в std:: basic_string при сохранении одинаковой функциональности и производительности. Счет будет сходить от 103 функций-членов только до 32 членов. Это означает, что изменения в реализации класса будут влиять только на 32 вместо 103 членов, и поскольку интерфейс будет сохранен, 71 бесплатные функции, которые могут реализовать остальную функциональность в терминах 32 членов, не должны быть изменены.

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

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

class ReallyComplex
{
public:
   ReallyComplex& operator+=( ReallyComplex const & rhs );
};
ReallyComplex operator+( ReallyComplex const & lhs, ReallyComplex const & rhs )
{
   ReallyComplex tmp( lhs );
   tmp += rhs;
   return tmp;
}

Нетрудно видеть, что независимо от того, как оригинальная operator+= выполняет свою задачу, свободный operator+ выполняет свою работу правильно. Теперь, при любых изменениях в классе, operator+= нужно будет обновить, но внешний operator+ останется незатронутым на всю оставшуюся жизнь.

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

ReallyComplex operator+( ReallyComplex lhs, ReallyComplex const & rhs )
{
   lhs += rhs;
   return lhs;
}

Ответ 2

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

Я настоятельно рекомендую прочитать Эффективный С++ от Scott Meyers, который объясняет, почему вы должны это делать и когда это уместно.

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

Ответ 3

Этот вопрос, как представляется, уже был адресован при ответе на другой вопрос.

Такие правила, как правило, являются хорошими слугами и плохими хозяевами, есть сделки. Является ли результат более подходящим, если вы применяете предлагаемое преобразование? Зачем? Я считаю, что предполагаемое преимущество заключается в том, что, уменьшая количество методов, непосредственно связанных с частными данными объекта, вы можете более легко понять его поведение и, следовательно, упростить его поддержку.

Я не считаю, что ваш следующий пример достигает этой цели. [Вы можете заметить, что приведенный выше пример "после" не будет компилироваться в любом случае, но это другая история.] Если мы настроим его для реализации внешних функций только с точки зрения публичных методов, а не внутреннего состояния (добавьте аксессор для значения) то, я думаю, у нас был некоторый выигрыш. Достаточно ли этого, чтобы оправдать работу. Мое мнение: нет. Я думаю, что преимущества предлагаемого перехода к внешней функции становятся намного больше, когда методы обновляют значения данных в объекте. Я хотел бы реализовать небольшое количество мутаторов, поддерживающих инвариантность, и реализовать основные "бизнес-методы" с точки зрения тех, - экстернализация этих методов - это способ обеспечить, чтобы они могли работать только с точки зрения мутаторов.

Ответ 4

Это сильно зависит от вашего дизайна и ситуации, в которой вы находитесь. Когда я пишу свой код, я действительно ставил функции public и protected в классы. Остальное - это деталь реализации, которая является частью файла .cpp.

Я часто использую идиом pImpl. На мой взгляд, оба подхода имеют следующие преимущества:

  • Чистый дизайн интерфейса

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

  • Отключение от реализации

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

  • Скрыть зависимости от других классов, которые используют ваш интерфейс

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

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