От начального до среднего уровня: Перегрузка операторов (IV)
Введение
В предыдущей статье «От начального до среднего уровня: Перегрузка операторов (III)» было объяснено и показано, как следует действовать, когда необходимо реализовать перегрузку логических операторов и операторов сравнения. Несмотря на всю простоту, которую я стараюсь придать статьям, чтобы каждый мог понять, как создавать собственные решения, тема, связанная с перегрузкой операторов, — одна из тех, что вызывают наибольшую путаницу у начинающих программистов. Так что, уважаемый читатель, не думайте, что вы уже знаете, как делать такие вещи, лишь потому, что прочитали предыдущую статью. Очень важно, чтобы вы практиковались и экспериментировали в разных ситуациях, чтобы правильно усвоить эту тему.
Что ж, если логические операторы и операторы сравнения уже могут вызывать немало путаницы и проблем, то, что мы рассмотрим здесь, будет ещё сложнее. Это связано с тем, что мы рассмотрим ещё один оператор, который можно перегружать. Я имею в виду оператор индексации, и, поскольку он редко используется сам по себе, мы также рассмотрим оператор присваивания. Итак, пришло время отложить в сторону всё, что может вас отвлекать, и сосредоточиться на том, что мы рассмотрим в этой статье. Как некоторые любят говорить: теперь начинается самое интересное.
Перегрузка операторов (IV)
В рамках того, что позволяет нам делать MQL5, то, что мы рассмотрим здесь, завершит тему перегрузки операторов. По крайней мере, в её самой простой части, поскольку существуют и другие возможности, которые делают эту тему гораздо более продвинутой и способны сильно запутать новичков. Мы будем рассматривать это по мере написания и публикации новых статей. Тем не менее, в моём другом профиле я рассмотрю один довольно интересный способ использования этой перегрузки. Это мы оставим на другой раз.
Итак, самая важная особенность оператора [], также известного как оператор индексации, заключается в том, что обычно его не реализуют изолированно. Когда мы перегружаем этот оператор, нам, как правило, также нужно перегружать оператор присваивания. Это связано с тем, что мы будем присваивать значение по индексу или считывать значение по индексу. В любом случае, мы не должны исключать возможность и даже необходимость реализовать сразу два оператора.
Поскольку этот оператор индексации может оказаться довольно запутанным, я постараюсь показать, как реализуется его перегрузка, чтобы всем было понятно, что именно делается. Итак, давайте начнём с чего-нибудь действительно очень простого. Это показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stList 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. int value; 10. //+----------------+ 11. public : 12. //+----------------+ 13. stList *operator[](const uchar arg) 14. { 15. Print("Value ", arg, " was passed as being an index to the operator []"); 16. 17. return GetPointer(this); 18. } 19. //+----------------+ 20. }; 21. //+------------------------------------------------------------------+ 22. void OnStart(void) 23. { 24. stList demo; 25. 26. demo[4] = 10; 27. } 28. //+------------------------------------------------------------------+
Код 01
Если вы попробуете скомпилировать этот код 01, который абсолютно ничего не делает, то увидите, что компилятор выдаст несколько ошибок, как показано на следующем рисунке.

Рисунок 01
На этом рисунке 01 показаны некоторые проблемы, которые вам, мой дорогой читатель, очень важно хорошо понять. Первая связана с ошибкой, показанной в первой строке рисунка 01. Эта ошибка возникает из-за того, что в строке 17 из кода 01 мы фактически пытаемся вернуть доступ к объекту класса, объявленного в строке 4. Однако эта ссылка будет напрямую связана с переменной, объявленной в строке 9. Не рекомендуется возвращать ссылку на приватный член класса. Такой подход нарушает принцип инкапсуляции, что делает код чрезвычайно опасным и затрудняет его исправление в случае ошибок. Нам нужно вернуть указатель, который мы затем сможем использовать.
Итак, в предыдущих статьях мы уже видели, как можно устранить этот тип ошибки, показанный на рисунке 01. Однако здесь мы не будем делать это таким образом. Мы пойдём другим путём — поначалу немного более сложным, но по мере того, как вы будете практиковать то, что мы здесь рассмотрим, он будет казаться вам всё проще и проще.
Хорошо, две последние ошибки на рисунке 01 напрямую связаны с оператором присваивания. Это связано с тем, что компилятор не может определить, как обработать присваивание в данной ситуации. Помните, что здесь мы пойдём другим путём, и поэтому поначалу некоторые вещи могут казаться не вполне понятными. Итак, давайте изменим код 01 так, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stList 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. int value; 10. //+----------------+ 11. public : 12. //+----------------+ 13. stList *operator[](const uchar arg) 14. { 15. Print("Value ", arg, " was passed as being an index to the operator []"); 16. 17. return GetPointer(this); 18. } 19. //+----------------+ 20. void operator=(const int arg) 21. { 22. Print("Value ", arg," is being assigned to the internal variable of the class."); 23. } 24. //+----------------+ 25. }; 26. //+------------------------------------------------------------------+ 27. void OnStart(void) 28. { 29. stList demo; 30. 31. demo[4] = 10; 32. } 33. //+------------------------------------------------------------------+
Код 02
Теперь компилятор сможет сгенерировать исполняемый файл. Запустив его в терминале MetaTrader 5, мы получим результат, показанный ниже.

Рисунок 02
Что ж, на первый взгляд это не кажется чем-то значительным. Но это не так. И я объясню вам причину, чтобы вы могли понять, что здесь произошло. Если посмотреть на строку 31, то можно увидеть нечто вполне привычное для тех, кто уже умеет работать с массивами. Однако, несмотря на это кажущееся сходство, здесь мы делаем нечто совсем иное, чем просто работаем с массивами.
По этой причине важно понять, что происходит здесь, в коде 02, прежде чем переходить к следующему шагу. Обратите внимание, что при выполнении кода 02 сначала выполняется код из строки 13, а уже потом — код из строки 20. Понимание этого важно для осмысления самой перегрузки, которая здесь реализуется. В зависимости от того, что вы пытаетесь сделать, эта последовательность может быть иной. Так что обращайте внимание на эти мелкие детали.
Хорошо, перейдём к следующему моменту. Поначалу мы будем двигаться очень медленно, чтобы вы могли понять каждую мелочь из того, что здесь делается. Следующий шаг — понять, что на самом деле делает строка 31 кода 02. Это можно увидеть в приведённом ниже фрагменте.
. . . 26. //+------------------------------------------------------------------+ 27. void OnStart(void) 28. { 29. stList demo; 30. 31. demo.operator[](4).operator=(10); 32. } 33. //+------------------------------------------------------------------+
Фрагмент 01
В этом фрагменте показано, как компилятор будет интерпретировать код, включённый в процедуру OnStart, в коде 02. Понять это и чётко уяснить эту идею очень важно для того, чтобы понять, почему код, который мы рассмотрим далее, действительно работает.
Хорошо, следующий шаг показан ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stList 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. int value; 10. //+----------------+ 11. public : 12. //+----------------+ 13. stList *operator[](const uchar arg) 14. { 15. Print("Value ", arg, " was passed as being an index to the operator []"); 16. 17. return GetPointer(this); 18. } 19. //+----------------+ 20. stList *operator=(const int arg) 21. { 22. value = arg; 23. 24. return GetPointer(this); 25. } 26. //+----------------+ 27. void Debug(const int arg) 28. { 29. Print("Debugging line ", arg); 30. Print(value); 31. } 32. //+----------------+ 33. }; 34. //+------------------------------------------------------------------+ 35. void OnStart(void) 36. { 37. stList demo; 38. 39. (demo[4] = 10).Debug(__LINE__); 40. } 41. //+------------------------------------------------------------------+
Код 03
А результат выполнения показан на следующем рисунке.

Рисунок 03
Отлично, у нас есть код, который корректно работает в рамках этого простого подхода. Прежде чем перейти к чему-то более сложному, я хочу объяснить некоторые детали, которые, возможно, в будущем окажутся для вас решающими, уважаемый читатель. Один из них связан именно с оператором присваивания. Чтобы вы могли по-настоящему понять, что я хочу объяснить, нам нужно наделить оператор индексации какой-нибудь практической функциональностью, пусть даже самой простой. По этой причине мы изменим код так, чтобы он приобрёл практическую ценность. Таким образом, мы получаем следующий код.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stDemo 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. union un_0 10. { 11. ulong u64; 12. uchar u8[8]; 13. }m_info; 14. //+----------------+ 15. public : 16. //+----------------+ 17. void operator=(const ulong arg) 18. { 19. m_info.u64 = arg; 20. } 21. //+----------------+ 22. void Debug(const int arg) 23. { 24. PrintFormat("Info is 0x%I64X", m_info.u64); 25. } 26. //+----------------+ 27. }; 28. //+------------------------------------------------------------------+ 29. void OnStart(void) 30. { 31. stDemo demo; 32. 33. demo = 0x1020304050607080; 34. 35. demo.Debug(__LINE__); 36. } 37. //+------------------------------------------------------------------+
Код 04
При выполнении код 04 даст результат, показанный ниже.

Рисунок 04
Обратите внимание, что в данном случае, в коде 04, перегрузка оператора присваивания реализована так, что возможно только одно присваивание. Мы также не сможем напрямую вызвать процедуру отладки в той же строке, в которой выполняем присваивание. Как видите, это можно сделать именно так. Однако на практике мы обычно поступаем по-другому, уважаемый читатель. На практике мы обычно возвращаем ссылку, чтобы можно было выполнять каскадные присваивания. Мы скоро к этому вернёмся.
Но сначала я хочу, чтобы вы посмотрели ещё на одну вещь. Обратите внимание, что в строке девять мы определяем объединение, а внутри него у нас есть статический массив. А теперь обратите внимание на то, что мы будем делать, потому что это поможет нам объяснить некоторые интересные детали использования оператора индексации вместе с оператором присваивания. Итак, мы ещё раз изменим код, и он будет выглядеть так, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stDemo 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. union un_0 10. { 11. ulong u64; 12. uchar u8[8]; 13. }m_info; 14. uchar m_index; 15. //+----------------+ 16. public : 17. //+----------------+ 18. stDemo *operator[](const uchar arg) 19. { 20. m_index = arg; 21. return GetPointer(this); 22. } 23. //+----------------+ 24. void operator=(const ulong arg) 25. { 26. m_info.u64 = arg; 27. } 28. //+----------------+ 29. void Debug(const int arg) 30. { 31. PrintFormat("Info is 0x%I64X", m_info.u64); 32. } 33. //+----------------+ 34. }; 35. //+------------------------------------------------------------------+ 36. void OnStart(void) 37. { 38. stDemo demo; 39. 40. demo = 0x1020304050607080; 41. demo.Debug(__LINE__); 42. demo[1] = 0xFA; 43. demo.Debug(__LINE__); 44. } 45. //+------------------------------------------------------------------+
Код 05
При выполнении кода 05 мы получим результат, показанный ниже.

Рисунок 05
Очевидно, что то, что мы видим на рисунке 05, — не тот результат, который мы хотели получить. Тогда почему получился такой результат? Причина именно в том, что мы снова не перегружаем оператор присваивания. Как это так? Это может показаться немного запутанным. На самом деле это гораздо проще, чем вы, возможно, себе представляете, уважаемый читатель. Поймите следующее: когда компилятор попытается использовать оператор присваивания, он увидит только то, что мы определяем в строке 24. При этом он будет трактовать использование структуры так, как показано в строке 40, а не так, как мы хотим сделать это в строке 42. Чтобы это изменить, нам нужно, чтобы код выглядел так, как показано ниже.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. class stDemo 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. union un_0 10. { 11. ulong u64; 12. uchar u8[8]; 13. }m_info; 14. uchar m_index; 15. //+----------------+ 16. public : 17. //+----------------+ 18. stDemo *operator[](const uchar arg) 19. { 20. m_index = arg; 21. return GetPointer(this); 22. } 23. //+----------------+ 24. void operator=(const uchar arg) 25. { 26. m_info.u8[m_index] = arg; 27. } 28. //+----------------+ 29. void operator=(const ulong arg) 30. { 31. m_info.u64 = arg; 32. } 33. //+----------------+ 34. void Debug(const int arg) 35. { 36. PrintFormat("Info is 0x%I64X", m_info.u64); 37. } 38. //+----------------+ 39. }; 40. //+------------------------------------------------------------------+ 41. void OnStart(void) 42. { 43. stDemo demo; 44. 45. demo = 0x1020304050607080; 46. demo.Debug(__LINE__); 47. demo[1] = 0xFA; 48. demo.Debug(__LINE__); 49. } 50. //+------------------------------------------------------------------+
Код 06
Хорошо, обратите внимание: сейчас мы создаём ещё одну перегрузку того же оператора.
Чтобы узнать больше о том, как работает перегрузка и как её можно реализовать, ознакомьтесь со статьёй «От начального до среднего уровня: Перегрузка»; в ней подробно объясняются все основные принципы и понятия, связанные с реализацией перегрузки. Что ж, по-видимому, код выглядит правильно и, в принципе, должен давать нужный нам результат. Однако, когда вы попытаетесь скомпилировать этот код 06, вы получите следующее сообщение компилятора.

Рисунок 06
Но что это за странность? Я не понимаю, почему компилятор сообщает об ошибке такого типа, ведь, судя по всему, код реализован совершенно правильно. Действительно, мой дорогой читатель, в наших глазах код реализован правильно. Проблема в том, что компилятор интерпретирует это не так. Рисунок 06 показывает причину. Обратите внимание, что компилятор не может определить, какую функцию или процедуру нужно вызвать, чтобы правильно скомпилировать строку 47. Это связано именно с тем, что существует несколько перегрузок оператора присваивания.
Я понимаю, что поначалу такая ситуация кажется несколько странной именно из-за того, что объяснялось в статье, где мы рассматривали перегрузку функций и процедур. Однако исправить ошибку, которую мы видим на рисунке 06, очень просто. Нам нужно лишь указать компилятору, какую из перегрузок следует использовать. Для этого мы изменим строку 47, как показано в следующем фрагменте.
. . . 40. //+------------------------------------------------------------------+ 41. void OnStart(void) 42. { 43. stDemo demo; 44. 45. demo = 0x1020304050607080; 46. demo.Debug(__LINE__); 47. demo[1] = (uchar) 0xFA; 48. demo.Debug(__LINE__); 49. } 50. //+------------------------------------------------------------------+
Фрагмент 02
Теперь компилятор сможет создать исполняемый файл. Таким образом, когда мы запустим этот исполняемый файл, получим результат, показанный на следующем рисунке.

Рисунок 07
Хорошо, теперь у нас есть отправная точка для понимания других аспектов, связанных с этими двумя операторами. Но, возможно, вы думаете, что индекс неверен. Если вы пришли к такому выводу, вам нужно лишь изменить код, как показано в следующем фрагменте.
. . . 23. //+----------------+ 24. void operator=(const uchar arg) 25. { 26. m_info.u8[sizeof(m_info) - m_index - 1] = arg; 27. } 28. //+----------------+ . . .
Фрагмент 03
Теперь, когда мы снова запустим код, результат будет таким, как показано ниже.

Рисунок 08
Очень интересно, не правда ли, уважаемый читатель? Однако то, что мы здесь рассмотрели, — это лишь базовая и наименее интересная часть применения перегрузки этих операторов. Так что, может, посмотрим что-нибудь поинтереснее, чтобы продвинуться немного дальше?
Итак, в статье «От начального до среднего уровня: очереди, списки и деревья (IV)» было показано, как можно реализовать двусвязный список. В том коде у нас есть возможность сделать то, что в тот момент ещё было невозможно. Это связано с тем, что у нас тогда ещё не было тех механизмов, которые мы рассмотрели здесь, в этой статье. И механизм, о котором я говорю, — это как раз перегрузка операторов индексации и присваивания. Однако есть одна небольшая деталь, которая заставляет нас сделать нечто немного иначе. Но давайте двигаться постепенно, чтобы всё было понятно и не стало сложнее, чем нужно.
Итак, давайте возьмем один из кодов, рассмотренных в упомянутой выше статье. Это позволит нам понять, как использовать перегрузку в подобной ситуации. Код, который мы будем использовать, показан ниже.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. template <typename T> class stList 005. { 006. private: 007. //+----------------+ 008. T info; 009. stList <T> *prev, 010. *start; 011. uint counter; 012. //+----------------+ 013. public: 014. //+----------------+ 015. stList(void) 016. :prev(NULL), 017. start(NULL), 018. counter(0) 019. {} 020. //+----------------+ 021. void Store(T arg, const uint index = 0xFFFFFFFF) 022. { 023. stList <T> *loc, 024. *ptr1 = start, 025. *ptr2 = NULL; 026. 027. for (uint c = 0; (ptr1 != NULL) && (c < index); ptr2 = ptr1, ptr1 = (*ptr1).start, c++); 028. 029. loc = new stList <T>; 030. (*loc).info = arg; 031. (*loc).start = (ptr2 != NULL ? (*ptr2).start : ptr1); 032. (*loc).prev = (ptr1 != NULL ? (*ptr1).prev : ptr2); 033. if (ptr2 != NULL) (*ptr2).start = loc; else start = loc; 034. if (ptr1 != NULL) (*ptr1).prev = loc; else prev = loc; 035. 036. counter++; 037. } 038. //+----------------+ 039. bool Restore(T &arg, const uint index = 0xFFFFFFFF) 040. { 041. if ((prev == NULL) || (start == NULL)) 042. return false; 043. 044. stList <T> *loc = (index < counter ? start : prev), 045. *ptr = NULL; 046. 047. for (uint c = 0; (loc != NULL) && (c < index) && (index < counter); ptr = loc, loc = (*loc).start, c++); 048. if (loc == NULL) return false; 049. 050. if (index == 0) 051. { 052. start = (*loc).start; 053. if (start != NULL) (*start).prev = NULL; 054. } else if (index >= (counter - 1)) 055. { 056. prev = (*loc).prev; 057. if (prev != NULL) (*prev).start = NULL; 058. } 059. else 060. { 061. (*ptr).start = (*loc).start; 062. (*loc).start.prev = ptr; 063. } 064. arg = (*loc).info; 065. delete loc; 066. counter--; 067. 068. return true; 069. } 070. //+----------------+ 071. bool Exclude(const uint index) 072. { 073. T tmp; 074. 075. return Restore(tmp, index); 076. } 077. //+----------------+ 078. void Debug(void) 079. { 080. Print("===== DEBUG ====="); 081. for (stList <T> *loc = start; loc != NULL; loc = (*loc).start) 082. PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).start, loc, (*loc).prev, (*loc).info); 083. Print("================="); 084. } 085. //+----------------+ 086. }; 087. //+------------------------------------------------------------------+ 088. void OnStart(void) 089. { 090. stList <char> list; 091. 092. list.Store(10); 093. list.Store(84); 094. list.Store(-6); 095. list.Store(47, 0); 096. 097. list.Debug(); 098. 099. list.Exclude(3); 100. list.Store(35, 2); 101. 102. list.Debug(); 103. 104. for (char info; list.Restore(info, 0);) 105. Print(info); 106. }; 107. //+------------------------------------------------------------------+
Код 07
При выполнении этого кода 07 мы получим результат, показанный на следующем рисунке.

Рисунок 09
А теперь будьте внимательны, мой дорогой читатель, потому что этот момент очень важен. Этот код 07 создаёт список, как мы уже видели в ранее упомянутой статье. Однако прямое использование перегрузки операторов в данном случае имеет свои последствия, если вы должным образом не измените способ реализации связного списка. Это может показаться несколько противоречивым, поскольку код 07 работает корректно. Однако в текущем виде, если вы попытаетесь как угодно реализовать перегрузку операторов, в итоге у вас возникнет серьёзная проблема или, как минимум, получится нечто, что на практике окажется не слишком полезным.
Чтобы внести все необходимые изменения, нужно объяснить ещё одну деталь, о которой мы пока не говорили. Тем не менее, мы можем реализовать более базовый вариант перегрузки, рассмотренной в этой статье, чтобы этот код 07 был хотя бы немного понятнее, когда мы позже снова к нему обратимся. В принципе, изменения, которые нам пришлось бы внести, сделали бы строки 95 и 100 несколько более понятными. Однако за это приходится платить.
Поэтому результат, который мы увидим, по крайней мере вначале, будет немного отличаться от показанного на рисунке 09. Однако по мере того, как мы будем продвигаться дальше и я объясню ещё одну деталь, которую здесь тоже можно использовать, конечный результат будет в точности таким, как на рисунке 09. Однако код процедуры OnStart будет полностью отличаться от приведённого в коде 07. И, конечно, также изменится значительная часть кода класса stList. Это связано с гораздо более интенсивным использованием перегрузки операторов.
Итак, давайте начнём вносить изменения. Первым делом мы удалим функцию Exclude, которая находится в строке 71 этого кода 07. Но почему мы удаляем эту функцию именно сейчас? Потому что это удаление будет лишь временным. И, как бы странно это ни казалось, функция Exclude затрудняет доступ и определение индекса внутри оператора индексации, из-за чего результаты становятся значительно сложнее для понимания. Позже мы снова добавим функцию Exclude, хотя использоваться она будет немного иначе.
Хорошо. Ниже вы можете увидеть новый код. Это переходный код между кодом 07 и кодом, к которому мы придём позже.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. template <typename T> class stList 005. { 006. private: 007. //+----------------+ 008. T info; 009. stList <T> *prev, 010. *start; 011. uint counter; 012. //+----------------+ 013. public: 014. //+----------------+ 015. stList(void) 016. :prev(NULL), 017. start(NULL), 018. counter(0) 019. {} 020. //+----------------+ 021. void Store(T arg, const uint index = 0xFFFFFFFF) 022. { 023. stList <T> *loc, 024. *ptr1 = start, 025. *ptr2 = NULL; 026. 027. for (uint c = 0; (ptr1 != NULL) && (c < index); ptr2 = ptr1, ptr1 = (*ptr1).start, c++); 028. 029. loc = new stList <T>; 030. (*loc).info = arg; 031. (*loc).start = (ptr2 != NULL ? (*ptr2).start : ptr1); 032. (*loc).prev = (ptr1 != NULL ? (*ptr1).prev : ptr2); 033. if (ptr2 != NULL) (*ptr2).start = loc; else start = loc; 034. if (ptr1 != NULL) (*ptr1).prev = loc; else prev = loc; 035. 036. counter++; 037. } 038. //+----------------+ 039. bool Restore(T &arg, const uint index = 0xFFFFFFFF) 040. { 041. if ((prev == NULL) || (start == NULL)) 042. return false; 043. 044. stList <T> *loc = (index < counter ? start : prev), 045. *ptr = NULL; 046. 047. for (uint c = 0; (loc != NULL) && (c < index) && (index < counter); ptr = loc, loc = (*loc).start, c++); 048. if (loc == NULL) return false; 049. 050. if (index == 0) 051. { 052. start = (*loc).start; 053. if (start != NULL) (*start).prev = NULL; 054. } else if (index >= (counter - 1)) 055. { 056. prev = (*loc).prev; 057. if (prev != NULL) (*prev).start = NULL; 058. } 059. else 060. { 061. (*ptr).start = (*loc).start; 062. (*loc).start.prev = ptr; 063. } 064. arg = (*loc).info; 065. delete loc; 066. counter--; 067. 068. return true; 069. } 070. //+----------------+ 071. // bool Exclude(const uint index) 072. // { 073. // T tmp; 074. 075. // return Restore(tmp, index); 076. // } 077. //+----------------+ 078. stList <T> *operator[](const uint arg) 079. { 080. stList <T> *loc = start; 081. 082. return loc; 083. } 084. //+----------------+ 085. void operator=(const T arg) 086. { 087. info = arg; 088. } 089. //+----------------+ 090. void Debug(void) 091. { 092. Print("===== DEBUG ====="); 093. for (stList <T> *loc = start; loc != NULL; loc = (*loc).start) 094. PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).start, loc, (*loc).prev, (*loc).info); 095. Print("================="); 096. } 097. //+----------------+ 098. }; 099. //+------------------------------------------------------------------+ 100. void OnStart(void) 101. { 102. stList <char> list; 103. 104. list.Store(10); 105. list.Store(84); 106. list.Store(-6); 107. list.Store(47, 0); 108. 109. list.Debug(); 110. 111. // list.Exclude(3); 112. list.Store(35, 2); 113. 114. list.Debug(); 115. 116. for (char info; list.Restore(info, 0);) 117. Print(info); 118. }; 119. //+------------------------------------------------------------------+
Код 08
Теперь, при выполнении кода 08, мы получаем результат, показанный ниже.

Рисунок 10
Однако обратите внимание на код 08, поскольку в строке 78 мы начинаем реализовывать перегрузку оператора индексации. Кроме того, в строке 85 мы уже реализовали первую перегрузку оператора присваивания. Несмотря на это, код процедуры OnStart по-прежнему остаётся практически тем же. Цель этого кода 08 как раз и состоит в том, чтобы сгенерировать рисунок 10. Таким образом, мы можем внести следующее изменение, в результате которого процедура OnStart будет немного отличаться, как вы можете видеть ниже.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. template <typename T> class stList 005. { 006. private: 007. //+----------------+ 008. T info; 009. stList <T> *prev, 010. *start; 011. uint counter; 012. //+----------------+ 013. public: 014. //+----------------+ 015. stList(void) 016. :prev(NULL), 017. start(NULL), 018. counter(0) 019. {} 020. //+----------------+ 021. void Store(T arg, const uint index = 0xFFFFFFFF) 022. { 023. stList <T> *loc, 024. *ptr1 = start, 025. *ptr2 = NULL; 026. 027. for (uint c = 0; (ptr1 != NULL) && (c < index); ptr2 = ptr1, ptr1 = (*ptr1).start, c++); 028. 029. loc = new stList <T>; 030. (*loc).info = arg; 031. (*loc).start = (ptr2 != NULL ? (*ptr2).start : ptr1); 032. (*loc).prev = (ptr1 != NULL ? (*ptr1).prev : ptr2); 033. if (ptr2 != NULL) (*ptr2).start = loc; else start = loc; 034. if (ptr1 != NULL) (*ptr1).prev = loc; else prev = loc; 035. 036. counter++; 037. } 038. //+----------------+ 039. bool Restore(T &arg, const uint index = 0xFFFFFFFF) 040. { 041. if ((prev == NULL) || (start == NULL)) 042. return false; 043. 044. stList <T> *loc = (index < counter ? start : prev), 045. *ptr = NULL; 046. 047. for (uint c = 0; (loc != NULL) && (c < index) && (index < counter); ptr = loc, loc = (*loc).start, c++); 048. if (loc == NULL) return false; 049. 050. if (index == 0) 051. { 052. start = (*loc).start; 053. if (start != NULL) (*start).prev = NULL; 054. } else if (index >= (counter - 1)) 055. { 056. prev = (*loc).prev; 057. if (prev != NULL) (*prev).start = NULL; 058. } 059. else 060. { 061. (*ptr).start = (*loc).start; 062. (*loc).start.prev = ptr; 063. } 064. arg = (*loc).info; 065. delete loc; 066. counter--; 067. 068. return true; 069. } 070. //+----------------+ 071. // bool Exclude(const uint index) 072. // { 073. // T tmp; 074. 075. // return Restore(tmp, index); 076. // } 077. //+----------------+ 078. stList <T> *operator[](const uint arg) 079. { 080. stList <T> *loc = start; 081. for (uint c = 0; (loc != NULL) && (c < arg); loc = (*loc).start, c++); 082. return loc; 083. } 084. //+----------------+ 085. void operator=(const T arg) 086. { 087. info = arg; 088. } 089. //+----------------+ 090. void Debug(void) 091. { 092. Print("===== DEBUG ====="); 093. for (stList <T> *loc = start; loc != NULL; loc = (*loc).start) 094. PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).start, loc, (*loc).prev, (*loc).info); 095. Print("================="); 096. } 097. //+----------------+ 098. }; 099. //+------------------------------------------------------------------+ 100. void OnStart(void) 101. { 102. stList <char> list; 103. 104. list.Store(10); 105. list.Store(84); 106. list.Store(-6); 107. list.Debug(); 108. list[0] = 47; 109. list.Debug(); 110. list[2] = 35; 111. 112. list.Debug(); 113. 114. for (char info; list.Restore(info, 0);) 115. Print(info); 116. }; 117. //+------------------------------------------------------------------+
Код 09
Теперь, когда вы запустите этот код 09 в терминале MetaTrader 5, результат будет таким, как показано ниже.

Рисунок 11
Да уж, какая странная вещь. Но я вижу, что результат отличается от того, который был на предыдущих рисунках. Неужели нет способа сохранить предыдущие результаты? Ну, я не знаю, зачем вам, мой дорогой читатель, делать что-то подобное, ведь в итоге всё станет гораздо труднее для понимания. Однако, прежде чем показать, как это можно сделать, я хочу, чтобы вы обратили внимание на то, что теперь сам оператор индексации позволяет обращаться к списку так, как если бы это был массив. То, что раньше было невозможно.
Но давайте предположим — и я хочу подчеркнуть это предельно ясно, поскольку этого не будет в приложении, — что вы хотите сохранить то же поведение, или результат, что и в коде 08, но при использовании процедуры OnStart из кода 09. Как мы могли бы это сделать? Итак, чтобы этого добиться, нам нужно слегка изменить код, как показано ниже.
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. template <typename T> class stList 005. { 006. private: 007. //+----------------+ 008. T info; 009. stList <T> *prev, 010. *start; 011. uint counter; 012. //+----------------+ 013. public: 014. //+----------------+ 015. stList(void) 016. :prev(NULL), 017. start(NULL), 018. counter(0) 019. {} 020. //+----------------+ 021. void Store(T arg, const uint index = 0xFFFFFFFF) 022. { 023. this[index > counter ? counter : index] = arg; 024. } 025. //+----------------+ 026. bool Restore(T &arg, const uint index = 0xFFFFFFFF) 027. { 028. if ((prev == NULL) || (start == NULL)) 029. return false; 030. 031. stList <T> *loc = (index < counter ? start : prev), 032. *ptr = NULL; 033. 034. for (uint c = 0; (loc != NULL) && (c < index) && (index < counter); ptr = loc, loc = (*loc).start, c++); 035. if (loc == NULL) return false; 036. 037. if (index == 0) 038. { 039. start = (*loc).start; 040. if (start != NULL) (*start).prev = NULL; 041. } else if (index >= (counter - 1)) 042. { 043. prev = (*loc).prev; 044. if (prev != NULL) (*prev).start = NULL; 045. } 046. else 047. { 048. (*ptr).start = (*loc).start; 049. (*loc).start.prev = ptr; 050. } 051. arg = (*loc).info; 052. delete loc; 053. counter--; 054. 055. return true; 056. } 057. //+----------------+ 058. // bool Exclude(const uint index) 059. // { 060. // T tmp; 061. 062. // return Restore(tmp, index); 063. // } 064. //+----------------+ 065. stList <T> *operator[](const uint arg) 066. { 067. stList <T> *loc, 068. *ptr1 = start, 069. *ptr2 = NULL; 070. 071. for (uint c = 0; (ptr1 != NULL) && (c < arg); ptr2 = ptr1, ptr1 = (*ptr1).start, c++); 072. 073. loc = new stList <T>; 074. (*loc).start = (ptr2 != NULL ? (*ptr2).start : ptr1); 075. (*loc).prev = (ptr1 != NULL ? (*ptr1).prev : ptr2); 076. if (ptr2 != NULL) (*ptr2).start = loc; else start = loc; 077. if (ptr1 != NULL) (*ptr1).prev = loc; else prev = loc; 078. 079. counter++; 080. 081. return loc; 082. } 083. //+----------------+ 084. void operator=(const T arg) 085. { 086. info = arg; 087. } 088. //+----------------+ 089. void Debug(void) 090. { 091. Print("===== DEBUG ====="); 092. for (stList <T> *loc = start; loc != NULL; loc = (*loc).start) 093. PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).start, loc, (*loc).prev, (*loc).info); 094. Print("================="); 095. } 096. //+----------------+ 097. }; 098. //+------------------------------------------------------------------+ 099. void OnStart(void) 100. { 101. stList <char> list; 102. 103. list.Store(10); 104. list.Store(84); 105. list.Store(-6); 106. list.Debug(); 107. list[0] = 47; 108. list.Debug(); 109. list[2] = 35; 110. 111. list.Debug(); 112. 113. for (char info; list.Restore(info, 0);) 114. Print(info); 115. }; 116. //+------------------------------------------------------------------+
Код 10
При использовании этого кода 10 помните, что ОН НЕ БУДЕТ В ПРИЛОЖЕНИИ по очевидным причинам; вы сможете получить результат, показанный ниже на рисунке 12.

Рисунок 12
Обратите внимание, что значительная часть этого рисунка 12 очень похожа на то, что мы видим на рисунке 10. Это показывает, что код 10 может дать тот же результат, что и код 08, но с использованием перегрузки операторов. Теперь обратите внимание на следующее в коде 10. Если вы посмотрите внутрь класса, то увидите, что содержимое, которое раньше находилось в процедуре Store, теперь находится в реализации перегруженного оператора индексации. Кроме того, процедура Store фактически перенаправляет обращение к перегруженному оператору. Увидев это, вы можете подумать так: процедура Store нам больше не нужна. Мы можем напрямую использовать код из следующего фрагмента, и список всё равно будет создан с тем же результатом, что показан на Рисунке 12.
. . . 098. //+------------------------------------------------------------------+ 099. void OnStart(void) 100. { 101. stList <char> list; 102. 103. list[0] = 10; 104. list[1] = 84; 105. list[2] = -6; 106. list.Debug(); 107. list[0] = 47; 108. list.Debug(); 109. list[2] = 35; 110. 111. list.Debug(); 112. 113. for (char info; list.Restore(info, 0);) 114. Print(info); 115. }; 116. //+------------------------------------------------------------------+
Фрагмент 04
Да, уважаемый читатель, вы можете использовать код из фрагмента 04. Тем не менее, хочу напомнить вам, что цель перегрузки операторов как раз в том, чтобы сделать код проще и понятнее. Но здесь происходит НЕ ЭТО. Глядя на этот фрагмент 04 и учитывая, что во многих случаях класс действительно будет находиться в заголовочном файле, вы можете ошибочно решить, что в связанном списке должно быть только три значения, а не пять, как можно убедиться, выполнив код.
И то, что, на мой взгляд, а также с точки зрения многих других программистов, сбивает с толку ещё сильнее, заключается в следующем. Если смотреть только на этот фрагмент 04, можно было бы подумать, что список начинается со значения 10 и заканчивается значением -6, и что при выполнении строк 107 и 109 эти значения будут заменены соответственно на 47 и 35. Именно с такими проблемами всегда нужно быть осторожными при реализации перегрузки операторов.
И именно по этой причине этот код 10 НЕ БУДЕТ ДОСТУПЕН В ПРИЛОЖЕНИИ. Он приведён здесь исключительно как любопытная деталь для этой статьи.
Последняя деталь перед завершением статьи
Очень хорошо, всё кажется идеальным. Всё выглядит очень красиво и замечательно. Однако есть небольшое ограничение, которое немного усложняет ситуацию, когда речь идёт о перегрузке оператора индексации в MQL5. По крайней мере, на момент написания этой статьи я не нашёл способа обойти это ограничение.
И это ограничение, если можно так выразиться, несколько досадно. Дело в том, что в MQL5 мы НЕ МОЖЕМ использовать указатели так же, как в таких языках, как C или даже C++. Это затрудняет некоторые ключевые моменты, когда речь идёт о перегрузке операторов. Я знаю, что многие, возможно, не поймут того, что я сейчас объясню, но те, кто уже некоторое время занимается программированием, прекрасно это поймут.
Когда мы перегружаем оператор индексации, то есть [], мы НЕ ВОЗВРАЩАЕМ значения так, как сделали это здесь, в этой статье. Обычно мы возвращаем ссылку на область памяти, где находится значение, которое хотим изменить. За исключением более сложных случаев, в которых действительно используется нечто очень похожее на то, что мы здесь рассмотрели.
Естественно, когда был показан код 03, многие, вероятно, уже весьма заинтересовались. То же самое, вероятно, произошло и тогда, когда был показан код 06. Но, без всякого сомнения, энтузиазм достиг своего пика на коде 09 или, возможно, на коде 10. Однако не знаю, заметили ли вы это, но во всех случаях мы используем перегрузку для записи и ни разу — для чтения. Почему? Причина именно в том ограничении MQL5, о котором я упоминал ранее: мы не можем использовать указатели, как в C или C++.
Чтобы проиллюстрировать это, я покажу, как мог бы выглядеть очень простой код на C++, использующий перегрузку. Вы можете увидеть его ниже.
01. //+------------------------------------------------------------------+ 02. #include <stdio.h> 03. //+------------------------------------------------------------------+ 04. template <typename T> class stDemo 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. T *u8; 10. //+----------------+ 11. public : 12. //+----------------+ 13. stDemo(const int arg) 14. { 15. u8 = new T[arg]; 16. } 17. //+----------------+ 18. T &operator[](const int arg) 19. { 20. return u8[arg]; 21. } 22. //+----------------+ 23. }; 24. //+------------------------------------------------------------------+ 25. int main(void) 26. { 27. stDemo <char> demo(8); 28. char v1; 29. 30. demo[3] = 10; 31. demo[1] = demo[3]; 32. v1 = demo[1]; 33. 34. printf("%d", v1); 35. 36. return 0; 37. } 38. //+------------------------------------------------------------------+
Код 11
Здесь, в коде 11, у нас есть полностью работоспособная перегрузка. Однако этот код — НЕ MQL5. Это C++. И я хочу обратить ваше внимание именно на строки 31 и, прежде всего, на строку 32. Обратите внимание, что в строке 32 мы считываем значение и сохраняем его в переменной, не принадлежащей классу. Однако в MQL5 мы не можем сделать это. По крайней мере, не таким образом.
В MQL5 вам нужно решить, хотите ли вы записывать или считывать отдельные значения с помощью оператора индексации. Однако, как мы видели в этой статье, его лучше использовать для записи данных, поскольку в случае чтения у нас было бы гораздо больше ограничений. Это ограничение при чтении отдельных значений можно обойти с помощью функции или процедуры, предназначенной исключительно для этой задачи, если перегрузка оператора индексации ориентирована на запись данных.
"Ничего себе, вы и правда умеете портить праздник. Я был так счастлив, рад и доволен, а тут пришли вы и разрушили всю мою радость. Но ничего страшного. Поскольку у меня тоже не так много опыта, я не могу жаловаться на то, что вы готовы объяснить некоторые детали. Правда, я прощу вас только в том случае, если вы покажете мне, как в MQL5 можно сделать нечто похожее на то, что делает этот код 11, написанный на C++".
Итак, ниже приведён код на MQL5, позволяющий получить тот же результат, что и код на C++, показанный в коде 11.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. template <typename T> class stDemo 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. T u8[]; 10. int m_index; 11. //+----------------+ 12. public : 13. //+----------------+ 14. stDemo(const int arg) 15. { 16. m_index = -1; 17. ArrayResize(u8, arg); 18. } 19. //+----------------+ 20. stDemo <T> *operator[](const int arg) 21. { 22. m_index = arg; 23. return &this; 24. } 25. //+----------------+ 26. void operator=(const T arg) 27. { 28. u8[m_index - 1] = arg; 29. } 30. //+----------------+ 31. const T GetInfo(void) const 32. { 33. return u8[m_index - 1]; 34. } 35. //+----------------+ 36. }; 37. //+------------------------------------------------------------------+ 38. void OnStart(void) 39. { 40. stDemo <char> demo(8); 41. char v1; 42. 43. demo[3] = 10; 44. demo[1] = demo[3].GetInfo(); 45. v1 = demo[1].GetInfo(); 46. 47. Print(v1); 48. } 49. //+------------------------------------------------------------------+
Код 12
А теперь обратите внимание. В этом коде 12, написанном на MQL5, у нас есть перегрузка оператора, ориентированная именно на запись данных во внутреннюю структуру объекта. По этой причине мы можем без особых сложностей как считывать, так и записывать данные — при условии, конечно, что вы используете функцию, указанную в строке 31. Однако обратите внимание: хотя этот код 12 даёт тот же результат, что и код 11, написанный на C++, он полностью отличается от следующего.
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. template <typename T> class stDemo 05. { 06. //+----------------+ 07. private : 08. //+----------------+ 09. T u8[]; 10. //+----------------+ 11. public : 12. //+----------------+ 13. stDemo(const int arg) 14. { 15. ArrayResize(u8, arg); 16. } 17. //+----------------+ 18. stDemo <T> *Set(const int arg, const T info) 19. { 20. u8[arg - 1] = info; 21. return &this; 22. } 23. //+----------------+ 24. T operator[](const int arg) 25. { 26. return u8[arg - 1]; 27. } 28. //+----------------+ 29. }; 30. //+------------------------------------------------------------------+ 31. void OnStart(void) 32. { 33. stDemo <char> demo(8); 34. char v1; 35. 36. demo.Set(3, 10); 37. demo.Set(1, demo[3]); 38. v1 = demo[1]; 39. 40. Print(v1); 41. } 42. //+------------------------------------------------------------------+
Код 13
В случае кода 13, также написанного на MQL5, у нас есть перегрузка оператора индексации, ориентированная именно на чтение данных из внутренней структуры объекта. Обратите внимание, что это простое изменение полностью меняет то, как вы будете работать и как вам придётся реализовывать код. Результат работы всех трёх фрагментов кода будет абсолютно одинаковым: в терминале — будь то консоль, командная строка или даже MetaTrader 5 — мы увидим выведенное значение 10. И так происходит во всех случаях.
Однако, возможно, вы смотрите на код 12 или даже на код 13 и думаете: а нельзя ли как-то объединить оба подхода? Или, ещё лучше: нельзя ли создать нечто похожее на то, что мы видим в коде 11, написанном на C++? Что ж, уважаемый читатель, к сожалению, ответ на этот вопрос — НЕТ. По крайней мере, я не нашёл способа напрямую реализовать такую перегрузку в MQL5. Есть и другой способ, но он предполагает сочетание кода, написанного на C++, с кодом, написанным на MQL5. Однако, на мой взгляд, это уже продвинутая тема, и в этой серии статей мы её рассматривать не будем. Возможно, мы рассмотрим этот вопрос в одной из будущих серий, посвящённой исключительно продвинутым материалам.
Заключительные замечания
В этой статье мы сделали первый шаг, чтобы показать, как можно реализовать перегрузку оператора индексации, а также оператора присваивания, стараясь предложить практичный и интересный подход для всех. Однако то, что мы здесь рассмотрели, — это лишь часть того, что я ещё хочу показать и что напрямую связано с перегрузкой этих операторов. В этой статье появился знаменитый код 07, но мы не рассмотрели, как получить тот же результат с помощью перегрузки операторов. В следующей статье мы сосредоточимся исключительно на этом вопросе. Так вы сможете убедиться, что, если правильно подойти к решению, можно без особых затруднений создать полноценный код с использованием перегрузки операторов. И в итоге код будет гораздо более читаемым. По крайней мере, я считаю именно так, поскольку, на мой взгляд, код, который мы рассмотрим в следующей статье, понять гораздо проще, чем код 07 из этой статьи.
В любом случае, постарайтесь изучить и попрактиковаться в том, что мы здесь рассмотрели. И если вам интересно продолжить обучение, не забывайте: скоро выйдет следующая статья. До следующего раза.
| Файл MQ5 | Описание |
|---|---|
| Код 01 | Базовая демонстрация |
| Код 02 | Базовая демонстрация |
| Код 03 | Базовая демонстрация |
| Код 04 | Базовая демонстрация |
| Код 05 | Базовая демонстрация |
| Код 06 | Базовая демонстрация |
| Code.cpp | Базовая демонстрация |
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/16951
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Пайплайны Codex: от Python до MQL5 для выбора индикаторов — анализ ETF XLF по нескольким кварталам с применением машинного обучения
Разработка инструментария для анализа Price Action (Часть 74): Создание советника MQL5 на основе буферов индикатора
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования