Español Português
preview
От начального до среднего уровня: Перегрузка операторов (I)

От начального до среднего уровня: Перегрузка операторов (I)

MetaTrader 5Примеры |
67 0
CODE X
CODE X

Введение

В предыдущей статье «От базового к среднему: очереди, списки и деревья (VIII)» я представил свой вариант одного из многих алгоритмов балансировки деревьев, которые можно реализовать. Как там объяснялось, цель каждого алгоритма балансировки — сделать дерево более эффективным с точки зрения поиска. Некоторые из них позволяют лучше адаптировать дерево к определённым типам поиска, тогда как другие делают его более подходящим для поисков иного рода.

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

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

Однако, вопреки тому, что думают многие, то, что мы здесь увидим, сделает программирование гораздо более увлекательным и приятным. Тем не менее при его реализации нужно соблюдать определённые меры предосторожности. Я постараюсь придерживаться как можно более простого подхода, когда это возможно. Большая часть того, что я буду объяснять, основана на моём опыте программирования на C/C++. Тем не менее я постараюсь объяснить всё как можно проще.

Теперь перейдём к основной теме статьи.


Перегрузка операторов (I)

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

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

Если вы следили за моими статьями, то, вероятно, заметили, что я стараюсь не использовать определённые средства, чтобы не приходилось постоянно объяснять, как они работают. Однако, поскольку эта серия статей призвана передать знания иного рода, я не вижу причин ограничиваться использованием простых средств. Мы можем немного углубиться в некоторые более продвинутые возможности языка. Таким образом, в будущем я смогу писать код так, чтобы, на мой взгляд, было проще анализировать то, что реализуется, без необходимости объяснять каждую деталь, поскольку именно этим мы и займёмся в данных статьях.

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

Чтобы правильно начать, нужно понять следующее: зачастую перегрузка операторов уже встроена в сам язык программирования. И вы часто ею пользуетесь, даже не осознавая этого. Не так ли? Хорошо, давайте тогда рассмотрим очень простой пример, в котором используется перегрузка операторов. Следующий код реализует этот пример.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     string sz1;
07. 
08.     sz1 = "Hello world.";
09.     sz1 = sz1 + "How has your day been ?";
10. 
11.     Print(sz1);
12. }
13. //+------------------------------------------------------------------+

Код 01

К этому моменту, думаю, все уже знают, что будет выведено в терминале MetaTrader 5 при выполнении этого кода 01. Поэтому нет необходимости показывать результат. Однако это не меняет того факта, что в коде 01, хотя вы, возможно, этого и не замечаете, используется перегрузка операторов. Это происходит в девятой строке, когда мы применяем оператор сложения к двум операндам типа string. Но подождите минутку. Это не перегрузка операторов, потому что я указываю компилятору объединить оба строковых значения в одну более длинную строку. Поэтому я не считаю это перегрузкой операторов. Хотя, на самом деле, я всё ещё не знаю точно, что это такое. Я опираюсь исключительно на то, что было объяснено, когда я изучал статью о перегрузке функций и процедур.

Что ж, мой дорогой читатель, как бы странно это ни казалось, в MQL5, как и в других языках, некоторые операторы перегружены самим языком. Всё это реализовано так, чтобы вы могли без труда выполнять определённые операции, делая код более читабельным и понятным. Поскольку этот механизм встроен в язык на нативном уровне, он позволяет выражению в девятой строке кода 01 заменить явный вызов функции или процедуры конкатенации строковых значений. Однако, по сути, оператор сложения не предназначен для конкатенации строк. Его цель — складывать два значения и получать третье. Тем не менее, благодаря перегрузке операторов, реализованной на уровне языка, компилятор знает, как использовать оператор сложения для конкатенации строковых значений.

Следующий код реализует эквивалентную версию кода 01 без использования перегрузки операторов, как показано ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     string sz1;
07. 
08.     sz1 = "Hello world.";
09.     StringConcatenate(sz1, sz1, "How has your day been ?");
10.     Print(sz1);
11. }
12. //+------------------------------------------------------------------+

Код 02

Обратите внимание, что код 02 даёт тот же результат, что и код 01. Единственное различие между ними как раз и заключается в девятой строке. Теперь посмотрите на оба варианта и ответьте совершенно честно: в каком из них легче понять, что именно делается? Думаю, большинство ответит, что в коде 01. Именно это и позволяет нам делать перегрузка операторов: с помощью оператора выражать операцию, которая в противном случае потребовала бы явного вызова функции или процедуры, делая код более читабельным.

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

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

Рисунок 01

Очень важно знать, какие операторы язык позволяет перегружать. Дело в том, что в зависимости от того, как реализован код, вам, возможно, придётся искать в нём, какую операцию оператор выполняет над данными на самом деле выполняет тот или иной оператор.

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

тип_возврата operator перегружаемый_оператор (список параметров)

Обратите внимание на приведённую выше информацию, дорогой читатель, потому что именно этот синтаксис нам придётся использовать ВСЕГДА, когда нам понадобится или захочется перегрузить оператор. Давайте теперь разберём, что означает каждый элемент. ТИП_ВОЗВРАТА определяет тип значения, которое будет возвращать функция operator. Если функция не возвращает никакого значения, мы будем использовать тип void, хотя на практике рассмотрим это немного позже. OPERATOR — это зарезервированное слово языка, которое указывает компилятору, что объявляется функция operator. OPERATOR обозначает оператор, который будет перегружен.

Чтобы узнать, какие операторы можно перегружать, смотрите Рисунок 01. Список параметров работает так же, как и в случаях, рассмотренных в первых статьях. Единственное отличие заключается в том, что в зависимости от оператора здесь может быть один или несколько параметров. На этом этапе понимание деления на группы помогает нам интерпретировать сам список параметров. Операторы бинарной группы требуют только одного параметра, тогда как операторы унарной группы не требуют ни одного. Однако другие операторы могут использовать разное количество параметров. Но к этому мы ещё вернёмся. Всему своё время.

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

Если вы только что присоединились, мы уже говорили о структурах. Вы можете начать со статьи «От базового к среднему: Struct (I)». Там мы начали объяснять, что такое структура и как её можно использовать. Однако я буду исходить из того, что вы уже знаете, что такое структура данных. Таким образом, мы можем сосредоточиться на перегрузке операторов.

Хороший способ начать понимать перегрузку операторов — сначала разобраться в самом понятии перегрузки. Это понятие было представлено в статье «От базового к среднему: Перегрузка». Однако там мы говорили только о перегрузке функций и процедур. Понимание этого поможет вам сделать следующий шаг. Итак, начнём с того, что можно считать классической моделью представления перегрузки операторов. Следующий код реализует эту модель.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     double m_r, m_i;
07. //+----------------+
08.     stComplex(): m_r(0), m_i(0) {}
09. //+----------------+
10.     stComplex(double r, double i): m_r(r), m_i(i) {}
11. //+----------------+
12.     stComplex add(const stComplex &arg1, const stComplex &arg2)
13.     {
14.         return stComplex(arg1.m_r + arg2.m_r, arg1.m_i + arg2.m_i);
15.     }
16. //+----------------+
17. };
18. //+------------------------------------------------------------------+
19. void OnStart(void)
20. {
21.     stComplex   a(2, 5),
22.                 b(8, -3),
23.                 c;
24. 
25.     c = c.add(a, b);
26. 
27.     PrintFormat("C = %.02f %c %.02fi", c.m_r, (c.m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
28. }
29. //+------------------------------------------------------------------+

Код 03

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

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

Рисунок 02

Как видите, понять, что здесь происходит, совсем не трудно. Однако мы пока не используем перегрузку операторов. Я просто хочу шаг за шагом показать, как всё работает. А теперь, мой дорогой читатель, обратите внимание на следующее. В строке 25 мы указываем, что нужно сложить два значения и сохранить результат в другой переменной. В том виде, в котором это реализовано, ясно видно, что добавление третьего значения в вычисление было бы довольно громоздким. Для этого нам пришлось бы добавить ещё несколько вызовов функции add, определённой в строке 12. В конечном итоге код стал бы довольно запутанным из-за растущего числа вызовов. Однако если мы внесём небольшое изменение, ситуация полностью изменится, поскольку мы сможем использовать оператор сложения для сложения двух комплексных чисел, как это делает функция add, определённая в строке 12.

"Но подождите. Вы хотите сказать, что в этом коде 03 можно использовать оператор сложения?". Да, уважаемый читатель, именно это я и хочу сказать. Однако я хочу, чтобы вы очень хорошо поняли, что именно я говорю. Поэтому мы изменим код, заменив вызов функции add в строке 25 оператором сложения. Следующий код реализует это изменение.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     double m_r, m_i;
07. //+----------------+
08.     stComplex(): m_r(0), m_i(0) {}
09. //+----------------+
10.     stComplex(double r, double i): m_r(r), m_i(i) {}
11. //+----------------+
12.     stComplex add(const stComplex &arg1, const stComplex &arg2)
13.     {
14.         return stComplex(arg1.m_r + arg2.m_r, arg1.m_i + arg2.m_i);
15.     }
16. //+----------------+
17. };
18. //+------------------------------------------------------------------+
19. void OnStart(void)
20. {
21.     stComplex   a(2, 5),
22.                 b(8, -3),
23.                 c;
24. 
25.     c = a + b;
26. 
27.     PrintFormat("C = %.02f %c %.02fi", c.m_r, (c.m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
28. }
29. //+------------------------------------------------------------------+

Код 04

Подождите. Работает ли этот код 04? Нет, мой дорогой читатель. Если вы попытаетесь его скомпилировать, то получите следующие ошибки компилятора.

Рисунок 03

Это связано с тем, что компилятор НЕ ЗНАЕТ, как складывать два комплексных числа. Или, точнее, он не знает, как работать с типом данных, созданным пользователем или программистом. Тем не менее даже в этом случае мы можем применить другой подход, помня о том, что каждый случай индивидуален. В этом случае мы могли бы использовать следующий код.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     double m_r, m_i;
07. //+----------------+
08.     stComplex(): m_r(0), m_i(0) {}
09. //+----------------+
10.     stComplex(double r, double i): m_r(r), m_i(i) {}
11. //+----------------+
12.     stComplex add(const stComplex &arg1, const stComplex &arg2)
13.     {
14.         return stComplex(arg1.m_r + arg2.m_r, arg1.m_i + arg2.m_i);
15.     }
16. //+----------------+
17. };
18. //+------------------------------------------------------------------+
19. void OnStart(void)
20. {
21.     stComplex   a(2, 5),
22.                 b(8, -3),
23.                 c;
24. 
25.     c = stComplex(a.m_r + b.m_r, a.m_i + b.m_i);
26. 
27.     PrintFormat("C = %.02f %c %.02fi", c.m_r, (c.m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
28. }
29. //+------------------------------------------------------------------+

Код 05

Я знаю, что строка 25 в коде 05 может показаться вам довольно странной, но она действительно работает. Это связано с тем, что мы используем конструктор из строки 10 для создания значения комплексного типа. Благодаря этому компилятор может работать с типом данных, с которым изначально не умел обращаться. Этот пример позволяет рассмотреть ещё одну операцию, которую многие считают относительно простой, но которая в зависимости от случая может оказаться довольно сложной: присваивание значения переменной с помощью оператора присваивания. Как правило, переменной можно присваивать только значения того же типа. Однако бывают ситуации, когда нам нужно присвоить значение одного типа переменной совершенно другого типа. При необходимости нам придётся перегрузить оператор присваивания; в противном случае код не будет работать.

Но вернёмся к нашему исходному вопросу: реализации нашей первой перегрузки оператора. Как мы уже видели, складывать два комплексных числа можно разными способами. Однако код 04 пока не позволяет выполнить эту операцию. А теперь я спрошу вас: как мы могли бы выполнить это сложение? Что ж, ответ находится в начале статьи. "Но как же так? Я этого не понимаю". Хорошо, помните, я упоминал, что для перегрузки оператора существует специальный синтаксис?

Итак, если у нас уже есть функция или процедура, выполняющая нужную нам операцию, мы можем повторно использовать её реализацию, чтобы перегрузить оператор. Недостаточно просто изменить имя: нужно изменить её объявление, чтобы оно соответствовало синтаксису тип_возврата operator перегружаемый_оператор (список параметров). Именно это и происходит с функцией, определённой в строке 12 кода 03, которая складывает два комплексных числа и возвращает результат. Если заменить её объявление на объявление с operator +, компилятор распознает её как перегрузку оператора сложения, и код 04 заработает. Возможно, вы думаете: «Но это кажется довольно сложным. Как указать компилятору, чтобы он это сделал?» Что ж, это как раз самая простая часть. Сравните реализацию кода 04 с реализацией следующего кода.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     double m_r, m_i;
07. //+----------------+
08.     stComplex(): m_r(0), m_i(0) {}
09. //+----------------+
10.     stComplex(double r, double i): m_r(r), m_i(i) {}
11. //+----------------+
12.     stComplex operator+(const stComplex &arg1)
13.     {
14.         return stComplex(m_r + arg1.m_r, m_i + arg1.m_i);
15.     }
16. //+----------------+
17. };
18. //+------------------------------------------------------------------+
19. void OnStart(void)
20. {
21.     stComplex   a(2, 5),
22.                 b(8, -3),
23.                 c;
24. 
25.     c = a + b;
26. 
27.     PrintFormat("C = %.02f %c %.02fi", c.m_r, (c.m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
28. }
29. //+------------------------------------------------------------------+

Код 06

"Я не понимаю. Код 06 действительно очень похож на код 04. Однако в строке 12 используется другое количество аргументов. Почему?". Дело в том, что в строке 25 используется оператор сложения. Однако при использовании перегрузки операторов строку 25 нужно интерпретировать иначе. И именно здесь многие новички чувствуют себя совершенно растерянными, когда сталкиваются с кодом, в котором используется перегрузка операторов. Для новичка выражение с оператором сложения — это не вызов перегруженной функции, а обычное сложение. Однако, и это вам нужно понять, мой дорогой читатель, компилятор не будет интерпретировать строку 25 так, как она записана в коде 06. На самом деле компилятор интерпретирует её как эквивалент следующего кода.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     double m_r, m_i;
07. //+----------------+
08.     stComplex(): m_r(0), m_i(0) {}
09. //+----------------+
10.     stComplex(double r, double i): m_r(r), m_i(i) {}
11. //+----------------+
12.     stComplex operator+(const stComplex &arg1)
13.     {
14.         return stComplex(m_r + arg1.m_r, m_i + arg1.m_i);
15.     }
16. //+----------------+
17. };
18. //+------------------------------------------------------------------+
19. void OnStart(void)
20. {
21.     stComplex   a(2, 5),
22.                 b(8, -3),
23.                 c;
24. 
25.     c = a.operator+(b);
26. 
27.     PrintFormat("C = %.02f %c %.02fi", c.m_r, (c.m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
28. }
29. //+------------------------------------------------------------------+

Код 07

Коды 06 и 07 обозначают одно и то же. Однако гораздо чаще это записывают так, как в коде 06. Тем не менее, как программист, вы должны интерпретировать и понимать код так же, как это будет делать компилятор. Для этого нужно рассмотреть эквивалентную форму кода 07, где строка 25 представляет собой вызов функции из строки 12. Я знаю, что поначалу это кажется гораздо более запутанным, чем есть на самом деле. Тем не менее, это соответствие позволяет понять, почему в строке 12 нам нужно передать функции только один аргумент. Иначе это было бы очень трудно объяснить как-то по-другому.

"Хм, отлично. Теперь мне действительно понравилось узнать что-то новое. Эта перегрузка операторов и правда кажется очень интересной. Но скажи, можно ли складывать два комплексных числа только таким образом? Разве нельзя, например, сложить комплексное число и целое число?".

Хорошо, это уже немного сложнее для понимания. Однако ответ — ДА. Мы можем сложить значение типа stComplex и целое число. Тем не менее, нужно различать два разных выражения: stComplex + целое число, где объект stComplex является левым операндом, и целое число + stComplex, где в этой позиции находится целое число. Однако, чтобы упростить объяснение, поскольку этот момент может быть не сразу понятен, мы изменим один аспект структуры stComplex. Для начала можно заметить, что мы разрешаем доступ к внутренним переменным извне структуры, а это, вообще говоря, не лучшая идея. Поэтому мы изменим код так, чтобы скрыть эти переменные внутри структуры. После этого мы сможем сначала реализовать операцию stComplex + целое число.

Первая часть изменений показана ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06. //+----------------+
07.     private :
08.         double m_r, m_i;
09. //+----------------+
10.     public  :
11. //+----------------+
12.         stComplex(): m_r(0), m_i(0) {}
13. //+----------------+
14.         stComplex(double r, double i): m_r(r), m_i(i) {}
15. //+----------------+
16.         stComplex operator+(const stComplex &arg1)
17.         {
18.             return stComplex(m_r + arg1.m_r, m_i + arg1.m_i);
19.         }
20. //+----------------+
21.         void Debug(void)
22.         {
23.             PrintFormat("Internal Value = %.02f %c %.02fi", m_r, (m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
24.         }
25. //+----------------+
26. };
27. //+------------------------------------------------------------------+
28. void OnStart(void)
29. {
30.     stComplex   a(2, 5),
31.                 b(8, -3),
32.                 c;
33. 
34.     c = a + b;
35.     c.Debug();
36. }
37. //+------------------------------------------------------------------+

Код 08

Итак, давайте теперь проверим, можно ли вычислить выражение stComplex + целое число, то есть сумму, левый операнд которой имеет тип stComplex, а правый — целое число. Для этого сначала нужно понять, как реализовать перегрузку функций и процедур. В начале статьи я упомянул, что эти знания понадобятся, и указал, в какой статье содержится эта информация. Как только вы узнаете, как реализовать такую перегрузку, мы сможем определить операцию для этих типов и такого порядка операндов. Для этого нам нужно лишь определить следующую процедуру.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06. //+----------------+
07.     private :
08.         double m_r, m_i;
09. //+----------------+
10.     public  :
11. //+----------------+
12.         stComplex(): m_r(0), m_i(0) {}
13. //+----------------+
14.         stComplex(double r, double i): m_r(r), m_i(i) {}
15. //+----------------+
16.         stComplex operator+(const stComplex &arg1)
17.         {
18.             return stComplex(m_r + arg1.m_r, m_i + arg1.m_i);
19.         }
20. //+----------------+
21.         stComplex add(const double arg2)
22.         {
23.             return stComplex(m_r + arg2, m_i);
24.         }
25. //+----------------+
26.         void Debug(void)
27.         {
28.             PrintFormat("Internal Value = %.02f %c %.02fi", m_r, (m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
29.         }
30. //+----------------+
31. };
32. //+------------------------------------------------------------------+
33. void OnStart(void)
34. {
35.     stComplex   a(2, 5),
36.                 b(8, -3),
37.                 c;
38. 
39.     c = a + b;
40.     c.Debug();
41. 
42.     c = c.add(4);
43.     c.Debug();
44. }
45. //+------------------------------------------------------------------+

Код 09

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

Рисунок 04

Это ясно показывает, что код 09 допускает выражение stComplex + целое число: левый операнд имеет тип stComplex, а правый — целочисленное значение. Однако здесь возникает вопрос, мой дорогой читатель, и именно в этом заключается вся сложность. Поэтому в коде 09 мы пока не реализовали перегрузку операторов напрямую. Прежде всего, мне нужно, чтобы вы поняли одну вещь: перестановка операндов даёт другое выражение. Реализация, подходящая для stComplex + целое число, не обязательно подходит для целое число + stComplex, и именно это различие определяет, сможет ли код скомпилироваться.

Обратите внимание на следующее: когда компилятор будет анализировать код для создания исполняемого файла, ему придётся разрешить перегрузку на основе типа и позиции каждого операнда. Следовательно, stComplex + целое число и целое число + stComplex не являются взаимозаменяемыми: в первом выражении stComplex является левым операндом, а во втором — целое число. Если не существует перегрузки, совместимой с используемым порядком, компилятор НЕ СОЗДАСТ исполняемый файл, поскольку не сможет разрешить эту операцию. Поэтому понимание перегрузки функций и процедур важно для более широкого использования перегрузки операторов. Возможно, вы думаете, что одно не имеет никакого отношения к другому, но всё взаимосвязано. Поэтому те понятия, которые поначалу казались не слишком осмысленными, теперь нужно правильно понять, потому что они начинают полностью складываться в единую картину.

А теперь обратите внимание: при переходе от кода 03 к коду 06 мы убираем один аргумент из вызова. Чтобы поддерживать и обратный порядок, нам нужно пойти немного дальше и посмотреть на это по-другому. Есть два разных случая: код 09 реализует выражение stComplex + целое число, где stComplex является левым операндом; следующий код реализует выражение целое число + stComplex, где левым операндом является целое число.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06. //+----------------+
07.     private :
08.         double m_r, m_i;
09. //+----------------+
10.     public  :
11. //+----------------+
12.         stComplex(): m_r(0), m_i(0) {}
13. //+----------------+
14.         stComplex(double r, double i): m_r(r), m_i(i) {}
15. //+----------------+
16.         stComplex operator+(const stComplex &arg1)
17.         {
18.             return stComplex(m_r + arg1.m_r, m_i + arg1.m_i);
19.         }
20. //+----------------+
21.         stComplex add(const double arg2)
22.         {
23.             return stComplex(m_r + arg2, m_i);
24.         }
25. //+----------------+
26.         void Debug(void)
27.         {
28.             PrintFormat("Internal Value = %.02f %c %.02fi", m_r, (m_i < 0 ? '-' : '+'), MathAbs(c.m_i));
29.         }
30. //+----------------+
31. };
32. //+------------------------------------------------------------------+
33. void OnStart(void)
34. {
35.     stComplex   a(2, 5),
36.                 b(8, -3),
37.                 c;
38. 
39.     c = a + b;
40.     c.Debug();
41. 
42.     c = b.add(4);
43.     c.Debug();
44. }
45. //+------------------------------------------------------------------+

Код 10

Действительно, при выполнении кода 10 мы получим результат, показанный на следующем рисунке.

Рисунок 05

Обратите внимание, что код 09 вычисляет выражение stComplex + целое число, тогда как код 10 вычисляет выражение целое число + stComplex. Хотя в обоих случаях используются одни и те же типы, порядок операндов различается: в первом случае stComplex находится слева от оператора +, а во втором — справа. По этой причине, если мы используем перегрузку операторов, необходимо предусмотреть обе сигнатуры по отдельности. Именно поэтому я и сказал, что эта тема несколько сложнее и запутаннее, хотя здесь нет ничего, что нельзя было бы объяснить и реализовать.

Итак, у нас есть два порядка операндов, которые мы должны поддерживать: stComplex + целое число и целое число + stComplex. Как можно поддержать оба варианта самым простым способом? Самое простое решение реализовано в коде 11, который полностью приведён ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06. //+----------------+
07.     private :
08.         double m_r, m_i;
09. //+----------------+
10.     public  :
11. //+----------------+
12.         stComplex(): m_r(0), m_i(0) {}
13. //+----------------+
14.         stComplex(double r, double i): m_r(r), m_i(i) {}
15. //+----------------+
16.         stComplex operator+(const stComplex &arg1)
17.         {
18.             return stComplex(m_r + arg1.m_r, m_i + arg1.m_i);
19.         }
20. //+----------------+
21.         stComplex operator+(const double arg2)
22.         {
23.             return stComplex(m_r + arg2, m_i);
24.         }
25. //+----------------+
26.         stComplex operator+=(const double arg2)
27.         {
28.             return stComplex(m_r += arg2, m_i);
29.         }
30. //+----------------+
31.         void Debug(void)
32.         {
33.             PrintFormat("Internal Value = %.02f %c %.02fi", m_r, (m_i < 0 ? '-' : '+'), MathAbs(m_i));
34.         }
35. //+----------------+
36. };
37. //+------------------------------------------------------------------+
38. void OnStart(void)
39. {
40.     stComplex   a(2, 5),
41.                 b(8, -3),
42.                 c;
43. 
44.     c = a + b;
45.     c.Debug();
46. 
47.     (c += 4).Debug();
48.     c = b + 4;
49.     c.Debug();
50. }
51. //+------------------------------------------------------------------+

Код 11

В данном случае я изменил код, чтобы добавить ещё один аспект. Однако главной темой по-прежнему остаётся перегрузка операторов. При выполнении кода 11 вы получите результат, показанный на следующем рисунке.

Рисунок 06

Благодаря этой реализации мы довольно просто и увлекательно охватили оба порядка операндов: stComplex + целое число, используемый в коде 09, и целое число + stComplex, используемый в коде 10. Однако то, что мы здесь рассмотрели, — это лишь самая элементарная основа того, что нам необходимо понять, чтобы перейти к следующим статьям.


Заключительные замечания

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

Однако я хочу напомнить вам кое-что, дорогой читатель. Если вы случайно сразу попали на эту статью и только начинаете изучать программирование на MQL5, рекомендую прочитать и изучить материалы предыдущих статей. Каждая из них содержит небольшую часть того объёма знаний, который вам понадобится, чтобы понимать то, что мы будем показывать в следующих статьях. В любом случае не забудьте изучить и отработать на практике то, что здесь объяснено, используя файлы, включённые в приложение. В следующей статье мы продолжим углубляться в перегрузку операторов, потому что веселье только начинается.

Файл MQ5 Описание
Код 01 Базовая демонстрация
Код 02 Базовая демонстрация
Код 03 Базовая демонстрация
Код 04 Базовая демонстрация
Код 05 Базовая демонстрация
Код 06 Базовая демонстрация
Код 07 Базовая демонстрация
Код 08 Базовая демонстрация

Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/16854

Прикрепленные файлы |
Anexo.zip (3.59 KB)
Моделирование рынка: Position View (XIX) Моделирование рынка: Position View (XIX)
Один из вопросов, который доставлял мне наибольший дискомфорт, заключается в том, что класс C_ElementsTrade содержит код для обращения к позициям. Не воспринимайте это как ошибку, потому что на самом деле это не так. Однако из-за этого некоторые задачи, которые нам предстоит выполнить позже, становятся более рискованными с точки зрения ошибок. Вся работа по реализации индикатора позиции велась с расчетом на его использование в сервисе репликации/моделирования. Однако при запуске в этой среде у нас не будет никакого доступа к реальной позиции. Следовательно, любой вызов библиотеки MQL5, предназначенный для получения данных о позиции, не окажет никакого влияния на код.
Моделирование рынка: Position View (XVIII) Моделирование рынка: Position View (XVIII)
В этой статье я показал, в максимально доступной форме, как можно модифицировать и доработать код, способный решать определённые задачи, при этом минимально изменяя уже существующий код. Мы добавим индикатор объема и одновременно не позволим пользователю или трейдеру удалять объекты, создаваемые индикатором позиции.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
От начального до среднего уровня: Очереди, списки и деревья (VIII) От начального до среднего уровня: Очереди, списки и деревья (VIII)
В этой статье мы рассмотрим, как реализовать алгоритм балансировки дерева. Здесь я представлю свой вариант реализации этого алгоритма. Существует множество других алгоритмов с той же целью. Тем не менее, у каждого из них есть свои преимущества и недостатки. Вам, мой дорогой читатель, предстоит изучить их и найти тот, который лучше всего соответствует вашим потребностям.