Язык программирования C++ для профессионалов

       

Друзья и члены


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

Рассмотрим простой класс X:

class X { // ...

X(int);

int m1(); int m2() const;

friend int f1(X&); friend int f2(const X&); friend int f3(X); };

Вначале укажем, что члены X::m1() и X::m2() можно вызывать только для объектов класса X. Преобразование X(int) не будет применяться к объекту, для которого вызваны X::m1() или X::m2():

void g() { 1.m1(); // ошибка: X(1).m1() не используется 1.m2(); // ошибка: X(1).m2() не используется }

Глобальная функция f1() имеет то же свойство (§4.6.3), поскольку ее параметр - ссылка без спецификации const. С функциями f2() и f3() ситуация иная:

void h() { f1(1); // ошибка: f1(X(1)) не используется f2(1); // нормально: f2(X(1)); f3(1); // нормально: f3(X(1)); }

Следовательно операция, изменяющая состояние объекта класса, должна быть членом или глобальной функцией с параметром-ссылкой без спецификации const. Операции над основными типами, которые требуют в качестве операндов адреса (=, *, ++ и т.д.), для пользовательских типов естественно определять как члены.

Обратно, если требуется неявное преобразование типа для всех операндов некоторой операции, то реализующая ее функция должна быть не членом, а глобальной функцией и иметь параметр типа ссылки со спецификацией const или нессылочный параметр. Так обычно обстоит дело с функциями, реализующими операции, которые для основных типов не требуют адресов в качестве операндов (+, -, || и т.д.).

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

При всех прочих равных условиях лучше все-таки остановиться на функции-члене. Можно привести такие доводы. Нельзя гарантировать, что когда-нибудь не будет определена операция обращения. Нельзя во всех случаях гарантировать, что будущие изменения не повлекут за собой изменения в состоянии объекта. Запись вызова функции-члена ясно показывает программисту, что объект может быть изменен, тогда как запись с параметром-ссылкой далеко не столь очевидна. Далее, выражения допустимые в функции-члене могут быть существенно короче эквивалентных выражений в глобальной функции. Глобальная функция должна использовать явно заданные параметры, а в функции-члене можно неявно использовать указатель this. Наконец, поскольку имена членов не являются глобальными именами, они обычно оказываются короче, чем имен глобальных функций.



Содержание раздела