Шаг 19.
Перегрузка стандартных операций (окончание)

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

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

    Можно ли вводить собственные обозначения для операций, не совпадающие со стандартными операциями языка C++?

    И все ли операции языка C++ могут быть перегружены?

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

Таблица 1. Операции, которые не перегружаются
Операция
Назначение
.(точка)
Прямой выбор компонента структурированного объекта.
.*
Обращение к компоненту через указатель на него.
?:
Условная операция.
::
Операция указания области видимости.
sizeof
Операция вычисления размера в байтах.
#
Препроцессорная операция.
##
Препроцессорная операция.

    Рассмотрим еще несколько важных особенностей механизма перегрузки (расширения действия) стандартных операций языка C++.

  1. При расширении действия (при перегрузке) стандартных операций нельзя и нет возможности изменять их приоритеты.
  2. Нельзя изменить для перегруженных операций синтаксис выражений, т.е. невозможно ввести унарную операцию "=" или бинарную операцию "++".
  3. Нельзя вводить новые лексические обозначения операций, даже формируя их из допустимых символов. Например, возведение в степень "**" из языка Fortran нельзя ввести в языке C++.
  4. Любая бинарная операция "@" определяется для объектов некоторого класса двумя существенно разными способами:
    • либо как компонентная функция с одним параметром,
    • либо как глобальная (возможно дружественная) функция с двумя параметрами.

        В первом случае X @ Y означает вызов X.operator @ (Y), во втором случае X @ Y означает вызов operator @ (X,Y).

        В соответствии с семантикой бинарных операций "=", "[ ]", "->" операции-функции с названиями operator =, operator [ ], operator -> не могут быть глобальными функциями, а должны быть нестатическими компонентными функциями.

  5. Любая унарная операция '$' определяется для объектов некоторого класса также двумя способами:
    • либо как компонентная функция без параметров,
    • либо как глобальная (возможно дружественная) функция с одним параметром.

        Для префиксной операции "$" выражение $z означает вызов компонентной функции z.operator $ () или вызов глобальной функции operator $ (z).

        Для постфиксной операции выражение z$ означает либо вызов компонентной функции z.operator $ (), либо вызов глобальной функции operator $ (z).

  6. Синтаксис языка C++ определяет некоторые встроенные операции над стандартными типами как комбинации других встроенных операций над теми же операндами. Например, для переменной long m = 0; выражение ++m означает m += 1, что в свою очередь означает выполнение выражения m = m + 1. Такие автоматические замены выражений не реализуются и не справедливы для перегруженных операций. Например, в общем случае определение operator *=() нельзя вывести из определений operator * () и operator = ().
  7. Нельзя изменить смысл выражения, если в него не входит объект класса, введенного пользователем. Невозможно для операнда m типа int изменить смысл выражения 2 + m и т.п.
  8. Операция-функция, первым параметром которой предполагается основной (стандартный) тип, не может быть компонентной функцией. Для объяснения этого ограничения предположим, что аа - объект некоторого класса и для него расширено действие операции "+".

        При разборе выражения аа + 2 компилятором выполняется вызов операции-функции аа.operator + (2) или operator + (aa,2).

        При разборе 2 + аа допустим вызов operator + (2,аа), но ошибочен 2.operator + (аа). Таким образом, расширение действия операции "+" на выражение

        стандартный_тип + объект_класса 
    
    допустимо только с помощью глобальных операций-функций.
  9. При расширении действия операций приходится предусматривать всевозможные сочетания типов операндов. Например, определяя операцию сложения "+" для комплексных чисел, приходится учитывать сложение комплексного числа с вещественным и вещественного с комплексным, комплексного с целым и целого с комплексным и т.д. Если учесть, что вещественные числа представлены несколькими типами (float, double, long double) и целые числа имеют разные типы (int, long, unsigned, char), то оказывается необходимым ввести большое количество операций-функций. К счастью, при вызове операций-функций действуют все соглашения о преобразованиях стандартных типов параметров, и нет необходимости учитывать сочетания всех типов. В ряде случаев для бинарной операции достаточно определить только три варианта:
    • стандартный тип, класс;
    • класс, стандартный_тип;
    • класс, класс.

    Например, для рассмотренного класса complex можно ввести как дружественные такие операции-функции:

complex operator + (complex x,   complex у)
  { return complex(x.real + у.real, x.imag + y.imag); }
complex operator + (double x,   complex y)
  { return complex(x + y.real, y.imag);   }
complex operator + (complex x,  double y)
  { return complex(x.real + y, x.imag);   }

    После этого станут допустимыми выражения в следующих операторах:

  complex CC(1.0,2.0); 
  complex ЕЕ; 
  ЕЕ = 4.0 + CC; 
  ЕЕ = ЕЕ + 2.0;
  ЕЕ = CC + ЕЕ;
  ЕЕ = CC + 20;   // По умолчанию приведение int к double.
  CC = ЕЕ + 'е';  // По умолчанию приведение char к double.

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

  complex(double x)
    { real = x;   imag = 0.0; }

позволяет удалить все дополнительные операции-функции, оставив только одну с прототипом:

  friend complex operator + (complex, complex);

    В этом случае целый операнд выражения 6 + ЕЕ автоматически преобразуется к типу double, а затем конструктор формирует комплексное число с нулевой мнимой частью. Далее выполняется операция-функция:

   operator + (complex(double(6),double(0)), ЕЕ)

    Вместо включения в класс дополнительного конструктора с одним аргументом можно в заголовке единственного конструктора ввести умалчиваемое значение второго параметра:

   complex(double  re,   double  im =  0.0) 
        {   real = re;   imag = im;   }

    Теперь каждое выражение с операцией "+", в которое входит, кроме объекта класса complex, операнд одного из стандартных типов, будет обрабатываться совершенно верно. Однако такое умалчивание является частным решением и не для всех классов пригодно.

    Можно было бы в качестве умалчиваемого значения мнимой части взять и число, отличное от нуля, но поведение объектов класса complex при сложении с данными стандартных типов оказалось бы неверным.

    На следующем шаге мы продолжим подводить итоги перегрузки операций.




Предыдущий шаг Содержание Следующий шаг