Готовый перевод Google C++ Style Guide / Google C++ Style Guide: Classes
Классы
Классы являются фундаментальной единицей кода в C++. Естественно, мы широко их используем. Данный раздел содержит список основных плюсов и минусов, о которых нужно помнить при написании класса.
Выполнение кода в конструкторах
Избегайте комплексной инициализации в конструкторах (в частности, инициализации способной закончиться ошибкой либо требующей вызова виртуального метода).
Определение:
Возможность выполнить инициализацию в теле конструктора.
Плюсы:
Удобство печати. Нет нужды беспокоится о том, инициализирован ли класс или нет.
Минусы:
Проблемы с выполнением работы в теле конструктора:
Для конструктора не существует простого способа сообщить об ошибках, кроме использования исключений (которые запрещены).
Если работа не удалась, то у нас имеется объект, чей код инициализации не выполнен, что влечет за собой неопределенное состояние.
Если при работе вызываются виртуальные методы, эти вызовы не будут переданы наслденику. Будущие изменения вашего класса могут значительно усугубить проблему, даже если ваш класс не унаследован в данный момент, что может привести к большим затруднениям.
Если кто-то создаёт глобальную переменную такого типа (что против правил, но всё же), код конструктора будет вызван до вызова main(), возможно, ломая некоторые неявные предположения в коде конструктора.
Вывод:
Конструкторы никогда не должны вызывать виртуальные функции или пытаться повлечь нефатальные ошибки. Если Ваш объект нуждается в нетривиальной инициализации, решите это созданием функции-фабрики или метода Init().
Конструкторы по умолчанию
Вы должны определить конструктор по умолчанию, если ваш класс определяет члены-переменные и не имеет других конструкторов. В противном случае компилятор сделает это за вас, плохо.
Определение:
Конструктор по умолчанию вызывается когда мы создаем экземпляр класса без аргументов. Он всегда вызывается при вызове new[] (для массивов).
Плюсы:
Инициализация структур по умолчанию, чтобы исключить "невозможные" значения, делает дебаггинг гораздо проще.
Минусы:
Дополнительная работа для вас, написание большего кода.
Вывод:
Если Ваш класс определяет поля и не имеет конструкторов, необходимо всё же создать стандартный конструктор (пусть в нём даже не будет аргументов). Он должен предпочитаемым образом инициализировать объект таким образом, чтобы его внутреннее содержимое было целостно и достоверно.
Причина для этого заключается в том, что, если у Вас нету конструкторов и не определён стандартный, то компилятор сгенерирует такой конструктор сам. И этот сгенерированный конструктор может неправильно инициализировать Ваш объект.
Если ваш класс наследуется от существующего класса но вы не добавляли новых переменных-членов, то конструктор по умолчанию не нужен.
Явные конструкторы
Используйте ключевое слово explicit для конструкторов с одним аргументом.
Определение:
В большинстве случаев, если конструктор принимает аргумент, то он может использоваться для конвертации. К примеру, если вы определяете Foo::Foo(string name) и затем передаёте некую строку в функцию, которая принимает Foo, то конструктор может быть вызван для конвертации строки в Foo и передачи результата в Вашу функцию. Это может быть удобным для Вас, но может послужить и источником проблем, когда переменные конвертируются и этим создаются новые объекты, хотя Вы этого и не хотели. Объявление конструктора явно предотвращает его косвенные вызовы как конвертера.
Преимущества:
Исключает нежелательные преобразования.
Минусы:
Не имеются.
Вывод:
Мы требуем, чтобы все конструкторы с одним аргументом были explicit. Всегда помещайте explicit перед такими конструкторами в объявлениях класа: explicit Foo(string name);
Исключением являются copy-конструкторы, которые в тех редких случаях, когда мы их разрешаем, не должны быть explicit. Классы, являющиеся прозрачными оболочками для других классов, также в списке исключений из этого требования. Такие исключения должны быть обязательно помечены комментариями.
Copy-контрукторы
Переопределяйте copy-конструктор и оператор присваивания только когда это необходимо. Иначе, выключайте их с помощью DISALLOW_COPY_AND_ASSIGN.
Определение:
Copy-конструктор и оператор присваивания используются для создания копий объектов. Copy-конструктор косвенно вызывается компилятором в некоторых ситуациях (например, передача объекта как значения).
Преимущества:
Copy-конструкторы делают проще копирование объектов. STL контейнеры требуют, чтобы всё содержимое было доступно для записи и копирования. Copy-конструкторы могут быть более эффективны, чем CopyFrom()-обработчики, потому как они совмещают определение класса с копированием, компилятор может игнорировать их в некоторых контекстах и они позволяют проще избегать выделение памяти heap.
Недостатки:
Косвенное копирование объектов в С++ является огромным источников ошибок и проблем производительности. Так же, оно затрудняет чтение кода, так как становится сложным отследить, какие объекты были переданы по значению, а какие - по ссылке и, как результат - где отражаются изменения в объекте.
Вывод:
Некоторые классы нуждаются в возможности копирования. Большинство же не должно иметь ни copy-конструктора, ни оператора присвоения. Во многих ситуациях, ссылка или указатель будут работать так же хорошо, как скопированное значение, но с большей скоростью. К примеру, Вы можете передать в функцию параметру по ссылку или указателю вместо передачи по значениям и Вы можете хранить указатели, а не объекты в STL контейнерах.
Если ваш класс должен быть копируемым, то лучше используйте создание собственного метода для копирования, такого как CopyFrom() или Clone(), так как такие методы не могут быть вызваны косвенно или случайно. Если собственный метод не может быть решением в Вашем случае (например, низкая производительность или ваш класс должен храниться по значению в STL контейнере), создайте и copy-конструктор, и оператор присваивания.
Если Ваш класс не нуждается в copy-конструкторе или операторе присвоения, Вам следует явно выключить их. Чтобы сделать это, добавьте для них определения-болванки в секцию private: Вашего класса, но не описывайте их (так, чтобы любой их вызов возвращал ошибку связи).
Для удобства также может быть использован макрос DISALLOW_COPY_AND_ASSIGN:
Затем, в классе Foo:
Структуры vs. Классы
Используйте структуры только для пассивных объектов, которые хранят данные; всё остальное должно быть классами.
Ключевые слова "struct" и "class" работают почти одинаково в C++. Мы добавляем наше собственное значение к каждому из них, так что следует просто применять подходящее ключевое слово к определяемому Вами типу.
Структуры должны применяться для пассивных объектов, которые хранят данные и могут иметь соответствующие константы, но у них нет никакой нужды в функциональности кроме чтения/записи данных. А запись/чтение полей может производиться напрямую. Методы не должны отсутствовать, но нудны они лишь для настройки полей данных, например, конструктор, деструктор, Initialize(), Reset(), Validate().
Если требуется больше функциональности, класс больше подходит. Если сомневаетесь, используйте класс.
Для совместимости с STL, Вы можете использовать структуры вместо классов для функторов и traits.
Учтите, что правила именования переменных в структурах и в классах различаются.
Наследование
Сочетание порой более подходит, чем наследование. Когда Вы используете наследование, делайте его public.
Определение:
Когда класс-потомок наследуется от класса-родителя, он получает определения всех данных и операций, которые определяет класс-родитель. На практике, существует два основных способа наследования в C++: implementation inheritance, при котором весь код родителя наследуется потомком, и interface inheritance, при котором потомок получает лишь имена методов родителя.
За:
Implementation уменьшает объем кода повторным использованием кода базового класса. Так как наследование - объявление во время компиляции, то и вы, и компилятор можете понять операцию и обнаружить ошибку. Interface наследование может быть использовано для программного обеспечения наличия конкретного метода API у класса-потомка. Опять же, компилятор может обнаружить ошибку в том случае, если класс не определяет нужный метод (link error).
Против:
Для implementation наследования: так как реализация кода разбита на классы родителя и потомка, может оказаться сложнее его понимание. Потомок не может перегружать функции, не являющиеся виртуальными, так что он не может менять реализацию. Родитель может также определить некоторые поля данных, тем самым определяя своё физическое расположение.
Решение:
Все наследования должны быть public. Если вы хотите сделать private наследование, вы должны включать экземпляр родителя как поле данных.
Не злоупотребляйте наследованиями. Сочетание часто больше подходит для решения. Пытайтесь ограничить использование наследования с помощью случая "является": Bar является подклассом Foo если можно обоснованно сказать, что Bar "является видом" Foo.
Сделайте деструктор виртуальным, если необходимо. Если Ваш класс имеет виртуальные методы, его деструктор обязан быть виртуальным.
Ограничивайте использование protected только для тех методов, к которым может понадобиться доступ из классов-потомков. Помните, что поля данных должны быть private.
При переопределении унаследованной виртуальной функции, явно укажите virtual в определении получаемого класса. Обоснование: если virtual опущено, читающий код человек должен будет проверить всех предков данного класа, чтобы определить, является ли функция virtual.
Множественное наследование
Только в крайне редких случаях множественное implementation наследование действительно может пригодиться. Мы позволяем множественное наследование только когда хотя бы один из родителей имеет реализацию. Все остальные родители должны быть чистыми интерфейсами, обозначенными суффиксом interface.
Определение:
Множественное наследование позволяет потомку иметь больше, чем одного родителя. Мы различаем родителей: интерфейсы и те, у которых есть реализация.
За:
Множественное наследование может позволить Вам использовать повторно куда больше кода, чем один интерфейс (см. Наследование).
Против:
Действительно очень и очень редко множественное наследование может на самом деле быть полезным. Когда множественное наследование может казаться решением, обычно бывают и другие, более явные и правильные выходы.
Решение:
Множественное наследование позволяется только когда все классы-родители, с возможным исключением первого, являются чистыми интерфейсами. Чтобы Убедиться, что они являются интерфейсами, они должны заканчиваться соответствующим суффиксом Interface.
Важно: В Windows есть исключения для этого правила.
Интерфейсы:
Классы, удовлетворяющие определённым условиям могут, но не обязаны, заканчиваться суффиксом Interface.
Определение:
Класс является чистым интерфейсом, если удовлетворяет следующим пунктам:
У него только чистые public virtual ("= 0") методы и статичные методы (но читайте ниже про деструктор)
Он не может содержать не статичные поля данных.
Для него не требуются конструкторы. Если конструктор описан, у него не должно быть аргументов и он должен быть protected.
Если это потомок, он может быть получен только от классов, которые удовлетворяют этим условиям и отмечены суффиксом Interface.
Класс-интерфейс не может быть инстанцирован из-за чистых virtual методов, которые он объявляет. Чтобы убедиться, что все реализации интерфейса могут быть корректно удалены, интерфейс также может предоставить virtual деструктор ( как исключение из первого правила, он не должен быть пустым). См. Страуструп, The C++ Programming Language, 3rd edition, секция 12.4 для подробностей.
За:
Метка Interface у класса даёт знать остальным, что нельзя добавлять реализации методов или не статичные поля данных. Это, кстати, очень важно в случае с множественным наследованием. Дополнительно, концепты интерфейсов прекрасно понимают Java-программисты.
Против:
Суффикс Interface увеличивает длину названия класса, что может усложнить чтение и понимание. Кроме того, содержимое интерфейсов может восприниматься как часть реализации, которая не должна быть доступна клиенту.
Решение:
Класс может заканчиваться на Interface только тогда, когда он удовлетворяет перечисленным требованиям. Мы не требуем обратного, но всё же: классы, которые удовлетворяют эти требованиям, не обязаны быть интерфейсами.
Оператор перегрузки
Не перегружайте операторы кроме редких, особенных случаев.
Определение
Класс может определять поведение операторов (таких как + или /), как если бы это был встроенный тип.
За:
Это может сделать код более интуитивно-понятным, потому что класс будет вести себя так же, как встроенные типы (как int, например). Перегруженные операторы выигрывают во внешнем виде у функций, которые названы более тяжело, таких как Equal(), Add(). Для корректной работы некоторых шаблонов действительно может понадобиться определить операторы.
Против:
В то время, как перегрузка операторов может сделать код более понятным, она имеет и недостатки:
Она может обмануть нас, заставляя думать, что дорогие операции на самом деле просты как встроенные.
Гораздо сложнее будет находить места вызова перегруженных операторов. Поиск Equal() куда проще поиска нужных ==.
Некоторые операторы работают и на указателях, позволяя образовываться ошибкам. Foo + 4 может выполнять одно, пока &Foo + 4 делает нечто совершенно иное. Компилятор не будет жаловаться на подобное, затрудняя дебаг.
Перегрузка также порождает неожиданные ветвления. К примеру, если класс перегружает унарный оператор &, он не может быть безопасно перегружен в будущем.
Решение
В большинстве случаев не стоит перегружать операторы. Среди всех, оператор присвоения (operator=) очень коварен, так что стоит его избегать. Вы можете определять функции вроде Equals() и CopeFrom(), если они Вам нужны. Кроме того, любой ценой избегайте опасный унарный оператор operator&, если существует возможность, что класс будет объявлен далее.
Тем не менее, могут быть некоторые случаи, где Вам действительно нужно перегрузить оператор, чтобы взаимодействовать с шаблонами или "стандартными" C++ классами (например, operator<<(ostream&, const&T) для ведения логов). Это применимо, если необходимость действительно существует. Но Вам всё же следует пытаться избегать подобного, где это возможно. В том числе, не перегружайте operator== и operator< только чтобы Ваш класс мог использоваться в STL контейнере; вместо этого, Вам следует создать equality и comparision функторы при объявлении контейнера.
Некоторые алгоритмы STL требуют перегрузки operator==, и Вы можете поступать так при условии, что причина будет отражена в документации.
См. также Copy-конструкторы и Перегрузка Функций
Управление доступом
Делайте поля данных private и предоставляйте доступ к ним через функции-аксессоры, если необходимо (по техническим причинам, мы позволяем полям данных тестовых классов-заглушек быть protected при использовании Google Test). Обычная переменная может быть названа foo_, а аксессор для неё - foo(). Вам может также понадобиться функция set_foo(). Исключения: stati const поля данных (обычно называемые kFoo) не должны быть private.
Определения аксессоров, как правило, описаны в заголовочном файле.
См. также Наследование и Имена Функций
Порядок определения
Используйте следущий порядок определений в классе: public: перед private:, методы перед полями данных и переменными, и т.д.
Определение Вашего класса должно начинаться с его секции public:, продолжаться секцией protected: и затем завершаться секцией private:. Если какие-то из них пусты, опускайте их.
В каждой секции определения должны быть в следующем порядке:
Typedef-ы и enum-ы
Константы (static const поля)
Конструкторы
Деструктор
Методу, включая статические
Поля данных (кроме static const)
Объявления дружественных функций должны быть всегда в секции private. Кроме того, макрос DISALLOW_COPY_AND_ASSIGN должен быть в самом конце секции private:; это должно быть последнее, что Вы указываете в классе. См. Copy-конструкторы.
Объявления методов в соответствующих .cc файлах должны по возможности максимально следовать порядку объявления.
Не помещайте громоздкие определения методов в описание класса. Обычно лишь простые или влияющие на производительность функции могут быть так определены. См. Inline Functions для подробностей.
Написание коротких функций
Предпочитайте небольшие, таргетированные функции.
Мы понимаем, что большие функции порой подходят, так что никаких жёстких рамок на длину функции не налагается. Но если функция разрастается более, чем на 40 строк, стоит задуматься, где её можно разбить, не нарушая общей структуры программы.
Даже если ваша большая функция наконец прекрасно работает, кто-то, изменяющий её спустя несколько месяцев, может добавить новое поведение. Это может послужить причиной ошибок, которые будет сложно отыскать. Создание функций небольшими и простыми упрощает другим людям чтение и изменение Вашего кода.
Вы можете найти большие и сложные функции во время работы с каким-нибудь кодом. Так не бойтесь изменять уже существующее: если работа с данной функцией сложна, если вы считаете, что найденные в ней ошибки сложны в устранении, или же хотите использовать некоторые её фрагменты в других местах, разбейте её на меньшие и более простые в обработке куски.
Внимание! Этот перевод, возможно, ещё не готов.
Его статус: идёт перевод
Переведено на Нотабеноиде
http://notabenoid.org/book/32830/140600
Переводчики: aereiae, xmorph, peterdemin, flyngate
Настройки
Готово:
