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

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

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

Введение

В предыдущей статье «От начального до среднего уровня: Перегрузка операторов (IV)» мы прошли своё «боевое крещение» в перегрузке операторов. В той статье было показано, что перегрузка операторов имеет определённые ограничения. Тем не менее, несмотря на существование этих ограничений, они никоим образом не мешают использовать перегрузку операторов, чтобы сделать код более читабельным и понятным для любого человека.

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

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

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


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

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

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

Итак, в статье «От начального до среднего уровня: очереди, списки и деревья (II)» я объяснил, как можно преобразовать очередь, реализованную в массиве, в очередь, использующую указатели для связывания своих элементов. Это преобразование открывало возможность появления новой структуры данных: списков. Однако это не совсем главное. Важно то, как реализовать тот же тип очереди, показанный в упомянутой выше статье, с помощью перегрузки операторов.

Реализовывать этот вариант очень увлекательно, и это вдыхает в код новую жизнь, делая его гораздо интереснее.

В качестве отправной точки мы используем один из фрагментов кода, рассмотренных в той статье. Он показан ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         T info;
09.         C_Demo <T> *prev;
10. //+----------------+
11.     public:
12. //+----------------+
13.         C_Demo(void)
14.             :prev(NULL)
15.         {}
16. //+----------------+
17.         void Push(T arg)
18.         {
19.             C_Demo <T> *loc;
20. 
21.             loc = new C_Demo <T>;
22.             (*loc).info = arg;
23.             (*loc).prev = prev;
24.             prev = loc;
25.         }
26. //+----------------+
27.         bool Pop(T &arg)
28.         {
29.             C_Demo <T> *loc;
30. 
31.             if (prev == NULL)
32.                 return false;
33. 
34.             loc = prev;
35.             arg = (*loc).info;
36.             prev = (*loc).prev;
37. 
38.             delete loc;
39. 
40.             return true;
41.         }
42. //+----------------+
43. };
44. //+------------------------------------------------------------------+
45. void OnStart(void)
46. {
47.     C_Demo <char> demo;
48. 
49.     demo.Push(10);
50.     demo.Push(84);
51.     demo.Push(-6);
52. 
53.     for (char info; demo.Pop(info);)
54.         Print(info);
55. };
56. //+------------------------------------------------------------------+

Код 01

При выполнении этого кода получается приведённый ниже результат.

Рисунок 01

Идея заключается в том, чтобы воссоздать этот же код 01 с помощью перегрузки операторов и сохранить результат, показанный на рисунке 01. Возможно, сейчас вы думаете: «Да ладно, это же безумие». Как этого добиться, не превратив код в полную неразбериху? Ну что ж, уважаемый читатель, скоро вы поймёте, как это делается. Но сначала давайте придадим этому коду более практический вид. На практике гораздо чаще код разделяют на несколько файлов, чем хранят его целиком в одном, как в коде 01. Итак, теперь у нас есть два файла. Как видите, такое разделение не меняет работу программы, хотя и делает код гораздо интереснее.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_01.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo.Push(10);
11.     demo.Push(84);
12.     demo.Push(-6);
13. 
14.     for (char info; demo.Pop(info);)
15.         Print(info);
16. };
17. //+------------------------------------------------------------------+

Код 02

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         T info;
09.         C_Demo <T> *prev;
10. //+----------------+
11.     public:
12. //+----------------+
13.         C_Demo(void)
14.             :prev(NULL)
15.         {}
16. //+----------------+
17.         void Push(T arg)
18.         {
19.             C_Demo <T> *loc;
20. 
21.             loc = new C_Demo <T>;
22.             (*loc).info = arg;
23.             (*loc).prev = prev;
24.             prev = loc;
25.         }
26. //+----------------+
27.         bool Pop(T &arg)
28.         {
29.             C_Demo <T> *loc;
30. 
31.             if (prev == NULL)
32.                 return false;
33. 
34.             loc = prev;
35.             arg = (*loc).info;
36.             prev = (*loc).prev;
37. 
38.             delete loc;
39. 
40.             return true;
41.         }
42. //+----------------+
43. };
44. //+------------------------------------------------------------------+

Код 03

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

Отлично. А теперь давайте сосредоточимся на перегрузке операторов. Тот, кто изучает C++, сталкивается с использованием двух хорошо известных сущностей — stdin и stdout. Они обеспечивают простой и удобный доступ к стандартному вводу или выводу. Тем не менее их можно использовать и в других довольно интересных ситуациях.

Хорошо, в MQL5 нет или, по крайней мере, не используется то же самое понятие Stdin и Stdout. Тем не менее ничто не мешает нам реализовать наш код так, чтобы использовать ту же самую концепцию.

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

Возвращаясь к теме, понятия Stdin и Stdout появляются в следующем коде.

01. //+------------------------------------------------------------------+
02. #include <iostream>
03. //+------------------------------------------------------------------+
04. int main()
05. {
06.     int i1, i2, sum;
07. 
08.     std::cout << "Summing two numbers.\n";
09.     std::cout << "Enter the first number: ";
10.     std::cin >> i1;
11.     std::cout << "Enter the second number: ";
12.     std::cin >> i2;
13.     sum = i1 + i2;
14.     std::cout << "The result is: " << sum << "\n";
15. }
16. //+------------------------------------------------------------------+

Код 04

Вам не нужно знать C++, чтобы понять этот код, поскольку знаний MQL5 вполне достаточно, чтобы составить общее представление о том, как он работает. Заметьте следующий паттерн в этом коде 04. В некоторых строках встречается следующее: std::cout, за которым следует оператор. В других строках мы также встречаем ещё один элемент: std::cin, за которым тоже следует оператор. А теперь давайте посмотрим, что это за странная штука, которая появляется в коде.

Во-первых, std::cout представляет собой стандартный вывод. Все данные, которые мы записываем, отправляются в стандартный вывод. В свою очередь, std::cin представляет собой стандартный ввод и работает очень похоже на std::cout. Разница в том, что std::cin соответствует стандартному вводу. Таким образом, любые данные, поступающие через стандартный ввод, направляются в какую-либо переменную или другой элемент кода.

Поскольку зачастую сами по себе std::cout и std::cin не слишком наглядны, мы используем оператор, который помогает понять, как направляются данные. В особой ситуации, такой как в строке 14 этого кода 04, этот же оператор повторяется несколько раз, показывая тем самым, как компилятор C++ должен интерпретировать выражения.

Хорошо, на этом заканчивается часть по C++, которую мы рассматриваем в этой статье. Теперь мы воспроизведём тот самый оператор, который показан в коде 04, но уже в MQL5. В результате наш код MQL5 приобретает несколько более необычный вид. Тем не менее, если перегрузка операторов хорошо продумана, код может стать гораздо более удобочитаемым. Я разовью эту идею немного дальше в другой статье, которую вскоре опубликую в своём другом профиле. Здесь мы ограничимся только самым базовым, поскольку учимся использовать перегрузку операторов.

Хорошо, давайте изменим заголовочный файл, показанный в коде 03. Теперь он выглядит так, как полностью показано ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         T info;
09.         C_Demo <T> *prev;
10. //+----------------+
11.     public:
12. //+----------------+
13.         C_Demo(void)
14.             :prev(NULL)
15.         {}
16. //+----------------+
17.         void operator<<(const T arg)
18.         {
19.             C_Demo <T> *loc;
20. 
21.             loc = new C_Demo <T>;
22.             (*loc).info = arg;
23.             (*loc).prev = prev;
24.             prev = loc;
25.         }
26. //+----------------+
27.         bool operator>>(T &arg)
28.         {
29.             C_Demo <T> *loc;
30. 
31.             if (prev == NULL)
32.                 return false;
33. 
34.             loc = prev;
35.             arg = (*loc).info;
36.             prev = (*loc).prev;
37. 
38.             delete loc;
39. 
40.             return true;
41.         }
42. //+----------------+
43. };
44. //+------------------------------------------------------------------+

Код 05

Просмотрите этот код 05 и сравните его с кодом 03. Обратите внимание, что изменение было очень незначительным. Тем не менее этих простых изменений более чем достаточно, чтобы запутать не одного из тех, кто называет себя программистом MQL5. Теперь код действительно становится гораздо интереснее. Так что внимательно следите за следующим шагом. Прежде всего вернитесь к коду 02. Вы понимаете этот код? Хорошо, теперь ознакомьтесь с приведённым ниже кодом 06.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_01.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13. 
14.     for (char info; demo >> info;)
15.         Print(info);
16. };
17. //+------------------------------------------------------------------+

Код 06

Ничего себе, что за безумие творится в этом коде 06? Ладно, должен признать: вы совершенно безумны. Если бы вы показали мне этот код 06 с самого начала, я бы, наверное, перестал читать эти статьи. Но, судя по тому, что вы делаете с кодом, мне кажется, что для вас этот подход — не более чем чистое развлечение. Вам нравится играть с кодом и с языком. Ну надо же, я и не знал, что в MQL5 можно программировать таким образом.

Что ж, уважаемый читатель, этот пример — лишь небольшая часть того, что мы действительно можем делать. И результат выполнения этого кода 06 точно такой же, как и у кода 02. На самом деле я намеренно разделил файл на два — основной и заголовочный, — чтобы наглядно показать вам реализованное здесь решение. Попытаться напрямую объяснить эту реализацию было бы практически невозможно.

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

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> struct C_Demo
05. {
06.     private:
07. //+----------------+
08.         T info[];
09. //+----------------+
10.     public:
11. //+----------------+
12.         void operator<<(const T arg)
13.         {
14.             T local[1];
15.             local[0] = arg;
16.             ArrayInsert(info, local, info.Size());
17.         }
18. //+----------------+
19.         bool operator>>(T &arg)
20.         {
21.             arg = NULL;
22. 
23.             if (info.Size() > 0)
24.             {
25.                 arg = info[0];
26.                 ArrayRemove(info, 0, 1);
27.                 return true;
28.             }
29.             return false;
30.         }
31. //+----------------+
32. };
33. //+------------------------------------------------------------------+

Код 07

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

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. // #include "Include\C_Demo_01.mqh"
05. #include "Include\C_Demo_02.mqh"
06. //+------------------------------------------------------------------+
07. void OnStart(void)
08. {
09.     C_Demo <char> demo;
10. 
11.     demo << 10;
12.     demo << 84;
13.     demo << -6;
14. 
15.     for (char info; demo >> info;)
16.         Print(info);
17. };
18. //+------------------------------------------------------------------+

Код 08

А теперь обратите внимание на следующее, уважаемый читатель. Если включена строка 4, мы получаем реализацию стека. Если же включена строка 5, мы получаем FIFO-очередь. НЕ следует включать обе строки, если только вы не измените заголовочные файлы, включённые во вложение.

Итак, если мы запустим код так, как показано выше, результат показан на следующем рисунке.

Рисунок 02

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

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


Связный список, полный веселья (I)

Прежде чем начать, я хочу напомнить вам следующее понятие, уважаемый читатель. Связный список устроен так, чтобы вести себя почти так же, как динамический массив. Именно по этой причине в предыдущей статье его можно было реализовать именно таким образом. Однако вот что здесь важно: НЕ СЛЕДУЕТ ПУТАТЬ СВЯЗНЫЙ СПИСОК С МАССИВОМ. У этих двух структур совершенно разные назначения.

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

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

Итак, опираясь на принципы и понятия, необходимые для реализации этого списка, ниже мы приводим полный исходный код, который будем использовать.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         uint        m_counter;
09.         T           m_info;
10.         C_Demo <T>  *m_prev,
11.                     *m_next;
12. //+----------------+
13.         void Store(T arg1, const uint arg2 = UINT_MAX)
14.         {
15.             C_Demo <T>  *loc = new C_Demo <T>,
16.                         *ptr1 = m_next,
17.                         *ptr2 = NULL;
18.             for (uint c = 0; (ptr1 != NULL) && (c < arg2); ptr2 = ptr1, ptr1 = (*ptr1).m_next, c++);
19. 
20.             (*loc).m_info = arg1;
21.             (*loc).m_next = (ptr2 != NULL ? (*ptr2).m_next : ptr1);
22.             (*loc).m_prev = (ptr1 != NULL ? (*ptr1).m_prev : ptr2);
23.             if (ptr1 != NULL) (*ptr1).m_prev = loc; else m_prev = loc;
24.             if (ptr2 != NULL) (*ptr2).m_next = loc; else m_next = loc;
25.             m_counter++;
26.         }
27. //+----------------+
28.         bool Restore(T &arg1, const uint arg2 = UINT_MAX)
29.         {
30.             if ((m_prev == NULL) && (m_next == NULL)) return false;
31.             C_Demo <T>  *loc = (arg2 < m_counter ? m_next : m_prev),
32.                         *ptr = NULL;
33. 
34.             for (uint c = 0; (loc != NULL) && (c < arg2) && (arg2 < m_counter); ptr = loc, loc = (*loc).m_next, c++);
35.             if (loc == NULL) return false;
36.             if (arg2 == 0)
37.             {
38.                 m_next = (*loc).m_next;
39.                 if (m_next != NULL) (*m_next).m_prev = NULL;
40.             }else if (arg2 >= (m_counter - 1))
41.             {
42.                 m_prev = (*loc).m_prev;
43.                 if (m_prev != NULL) (*m_prev).m_next = NULL;
44.             }else
45.             {
46.                 (*ptr).m_next = (*loc).m_next;
47.                 (*loc).m_next.m_prev = ptr;
48.             }
49.             arg1 = (*loc).m_info;
50.             delete loc;
51.             m_counter--;
52.             m_prev = (m_counter ? m_prev : NULL);
53. 
54.             return true;
55.         }
56. //+----------------+
57.     public:
58. //+----------------+
59.         C_Demo() : m_counter(0), m_next(NULL), m_prev(NULL) {}
60. //+----------------+
61.         void operator<<(const T arg)
62.         {
63.             Store(arg);
64.         }
65. //+----------------+
66.         bool operator>>(T &arg)
67.         {
68.             return Restore(arg); // Режим стека
69.             // return Restore(arg, 0); // режим FIFO
70.         }
71. //+----------------+
72. };
73. //+------------------------------------------------------------------+

Код 09

Чтобы протестировать этот код, мы используем код 08. Конечно, с небольшим изменением. Итак, поскольку я не хочу слишком усложнять объяснение, уважаемый читатель, ниже приведён код, который мы используем.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_03.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13. 
14.     for (char info; demo >> info;)
15.         Print(info);
16. };
17. //+------------------------------------------------------------------+

Код 10

Как странно, этот код 10, похоже, на самом деле является кодом 06. На самом деле это не просто кажется: это и есть код 06, уважаемый читатель. Единственное отличие заключается в том, что в четвёртой строке мы используем другой файл. А теперь обратите внимание на эту деталь. Если вы рассмотрите код 09, то в строках 68 и 69 есть две закомментированные строки. Эти строки закомментированы, потому что я хочу, чтобы вы попробовали обе ситуации. В зависимости от того, какую из них мы используем, при выполнении кода 10 получаем немного иной результат. Я не буду приводить этот результат здесь, так как считаю, что в этом нет необходимости.

Попробуйте выполнить код 10, сначала используя строку 68 из кода 09, а затем строку 69. Результаты очень похожи на то, что показано на рисунках в предыдущей теме. Это сходство неудивительно, поскольку такое поведение уже демонстрировалось ранее. Ну, если вы не знаете или не имеете ни малейшего представления, о чём я говорю, ознакомьтесь со статьями об очередях, списках и деревьях.

Поскольку теперь код 09 реализует связный список, мы можем ещё немного доработать код списка, чтобы сделать его полностью функциональным. Для этого теперь добавим оператор индексации, как мы уже рассматривали в предыдущей статье. С этим изменением новый код приведён ниже.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         uint        m_counter;
09.         T           m_info;
10.         C_Demo <T>  *m_prev,
11.                     *m_next;
12. //+----------------+
13.         void Store(T arg1, const uint arg2 = UINT_MAX)
14.         {
15.             C_Demo <T>  *loc = new C_Demo <T>,
16.                         *ptr1 = m_next,
17.                         *ptr2 = NULL;
18.             for (uint c = 0; (ptr1 != NULL) && (c < arg2); ptr2 = ptr1, ptr1 = (*ptr1).m_next, c++);
19. 
20.             (*loc).m_info = arg1;
21.             (*loc).m_next = (ptr2 != NULL ? (*ptr2).m_next : ptr1);
22.             (*loc).m_prev = (ptr1 != NULL ? (*ptr1).m_prev : ptr2);
23.             if (ptr1 != NULL) (*ptr1).m_prev = loc; else m_prev = loc;
24.             if (ptr2 != NULL) (*ptr2).m_next = loc; else m_next = loc;
25.             m_counter++;
26.         }
27. //+----------------+
28.         bool Restore(T &arg1, const uint arg2 = UINT_MAX)
29.         {
30.             if ((m_prev == NULL) && (m_next == NULL)) return false;
31.             C_Demo <T>  *loc = (arg2 < m_counter ? m_next : m_prev),
32.                         *ptr = NULL;
33. 
34.             for (uint c = 0; (loc != NULL) && (c < arg2) && (arg2 < m_counter); ptr = loc, loc = (*loc).m_next, c++);
35.             if (loc == NULL) return false;
36.             if (arg2 == 0)
37.             {
38.                 m_next = (*loc).m_next;
39.                 if (m_next != NULL) (*m_next).m_prev = NULL;
40.             }else if (arg2 >= (m_counter - 1))
41.             {
42.                 m_prev = (*loc).m_prev;
43.                 if (m_prev != NULL) (*m_prev).m_next = NULL;
44.             }else
45.             {
46.                 (*ptr).m_next = (*loc).m_next;
47.                 (*loc).m_next.m_prev = ptr;
48.             }
49.             arg1 = (*loc).m_info;
50.             delete loc;
51.             m_counter--;
52.             m_prev = (m_counter ? m_prev : NULL);
53. 
54.             return true;
55.         }
56. //+----------------+
57.     public:
58. //+----------------+
59.         C_Demo() : m_counter(0), m_next(NULL), m_prev(NULL) {}
60. //+----------------+
61.         void operator<<(const T arg)
62.         {
63.             Store(arg);
64.         }
65. //+----------------+
66.         bool operator>>(T &arg)
67.         {
68.             return Restore(arg); // Режим стека
69.             // return Restore(arg, 0); // режим FIFO
70.         }
71. //+----------------+
72.         C_Demo <T> *operator[](const uint arg)
73.         {
74.             C_Demo <T> *loc = m_next;
75.             for (uint c = 0; (loc != NULL) && (c < arg); loc = (*loc).m_next, c++);
76. 
77.             return loc;
78.         }
79. //+----------------+
80.         void operator=(const T arg)
81.         {
82.             m_info = arg;
83.         }
84. //+----------------+
85.         void Debug(void)
86.         {
87.             Print("===== DEBUG =====");
88.             for (C_Demo <T> *loc = m_next; loc != NULL; loc = (*loc).m_next)
89.                 PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).m_next, loc, (*loc).m_prev, (*loc).m_info);
90.             Print("=================");
91.         }
92. //+----------------+
93. };
94. //+------------------------------------------------------------------+

Код 11

Чтобы проверить этот код 11, мы используем приведённый ниже код.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_03.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo.Debug();
14.     demo[0] = 47;
15.     demo.Debug();
16.     demo[2] = 35;
17.     demo.Debug();
18. 
19.     for (char info; demo >> info;)
20.         Print(info);
21. };
22. //+------------------------------------------------------------------+

Код 12

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

Рисунок 03

А теперь внимательно посмотрите на рисунок 03, уважаемый читатель. Обратите внимание: хотя наша основная цель — добавить значения 47 и 35 в список, на самом деле эти значения заменяют те, которые уже в нём есть. В зависимости от вашей реализации этот результат может идеально соответствовать тому, что вам нужно.

Однако мы НЕ ХОТИМ ЗАМЕНЯТЬ существующие значения. Мы хотим добавить в список новые значения. Итак, теперь я представляю своё решение этой проблемы. Не пугайтесь, увидев код, уважаемый читатель. Далее я объясню, как и почему это работает, поскольку понимание принципа работы важнее самого кода.

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

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         T           m_info;
09.         C_Demo <T>  *m_prev,
10.                     *m_next;
11. //+----------------+
12. #define def_Adjust(x) (m_prev = (m_prev == NULL ? x : ((*m_prev).m_prev != NULL ? (*m_prev).m_prev : m_prev)))
13. //+----------------+
14.     public:
15. //+----------------+
16.         C_Demo() : m_next(NULL), m_prev(NULL) {}
17. //+----------------+
18.         void operator<<(const T arg)
19.         {
20.             C_Demo <T>  *loc = new C_Demo <T>;
21.                         
22.             (*loc).m_info = arg;
23.             (*loc).m_prev = m_next;
24.             if (m_next != NULL) (*m_next).m_next = loc;
25.             m_next = loc;
26.             m_prev = def_Adjust(loc);
27.         }
28. //+----------------+
29.         bool operator>>(T &arg)
30.         {
31.             C_Demo <T>  *loc = def_Adjust(m_prev);
32.             if (loc == NULL) return false;
33.             arg = (*loc).m_info;
34.             if ((m_prev = (*loc).m_next) != NULL) (*m_prev).m_prev = NULL;
35.             delete loc;
36. 
37.             return true;
38.         }
39. //+----------------+
40.         C_Demo <T> *operator[](const uint arg)
41.         {
42.             C_Demo <T> *loc = def_Adjust(m_prev);
43.             for (uint c = 0; (loc != NULL) && (c < arg); loc = (*loc).m_next, c++);
44.             return loc;
45.         }
46. //+----------------+
47.         void operator=(const T arg)
48.         {
49.             m_info = arg;
50.         }
51. //+----------------+
52.         void operator<<=(const T arg)
53.         {
54.             C_Demo <T>  *loc = new C_Demo <T>;
55.             
56.             (*loc).m_info = arg;
57.             (*loc).m_next = GetPointer(this);
58.             (*loc).m_prev = m_prev;
59.             if (m_prev != NULL) (*m_prev).m_next = loc;
60.             m_prev = loc;
61.         }
62. //+----------------+
63.         void Debug(uint line)
64.         {
65.             Print("===== DEBUG [", line, "]=====");
66.             for (C_Demo <T> *loc = def_Adjust(m_prev); (loc != NULL); loc = (*loc).m_next)
67.                 PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).m_prev, loc, (*loc).m_next, (*loc).m_info);
68.             Print("=================");
69.         }
70. //+----------------+
71. #undef def_Adjust
72. //+----------------+
73. };
74. //+------------------------------------------------------------------+

Код 13

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

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_04.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo.Debug(__LINE__);
14.     demo[2] <<= 35;
15.     demo.Debug(__LINE__);
16.     demo[0] <<= 47;
17.     demo.Debug(__LINE__);
18.     demo[0] <<= 110;
19.     demo.Debug(__LINE__);
20. 
21.     for (char info; demo >> info;)
22.         Print(info);
23. };
24. //+------------------------------------------------------------------+

Код 14

"Ради БОГА и всего самого святого. Неужели вам совсем не жалко нас, бедных читателей? Что это за бессмысленный и нелепый код вы показываете? Да уж, я в жизни не видел ничего настолько сложного. Теперь я точно уверен: вы СУМАСШЕДШИЙ". Успокойтесь, уважаемый читатель. Как я уже сказал, я объясню происходящее здесь. Но сначала давайте посмотрим, каков результат выполнения этого кода 14. Итак, результат показан на следующем рисунке.

Рисунок 04

Обратите внимание, что теперь у нас действительно есть то, что мы хотим. Мы ещё не закончили. Прежде чем продолжить, давайте разберёмся, как работают код 13 и код 14. Очевидно, если вы сначала проанализируете код 14, то благодаря объяснениям из предыдущего раздела уже сможете составить некоторое представление о том, что происходит. Однако строки 14, 16 и 18 этого кода 14 могут вас несколько озадачить, поскольку выглядят как полное безумие. Как я уже говорил, это моё предложение по решению проблемы. И, как показывает рисунок 04, это решение действительно работает. Вопрос в том: КАК это работает? Итак, чтобы понять этот механизм, нам нужно теперь проанализировать код 13.

В код 13 были внесены несколько более существенные изменения, чтобы сделать его компактнее. В то же время я настроил вывод в режиме FIFO. Теперь принцип работы немного отличается от предыдущего. Разница объясняется одной деталью, использованной в этом коде 13, о которой я ещё не говорил. Однако очень важно правильно это понять, чтобы разобраться, почему работает код 14.

В коде есть только два цикла: один в строке 43, а другой — в строке 66. И почему эти циклы так важны? Потому что, если вы не поймёте их назначение, вы НЕ ПОЙМЁТЕ, как работает перегрузка операторов, которая показана в строке 52. Более того, если вы не до конца понимаете, что мы делали в предыдущем разделе, моё объяснение покажется вам гораздо более запутанным, чем оно есть на самом деле. Начнём с самого начала.

В строке 18 представлена перегрузка операторов, добавляющая новые элементы в двусвязный список. Способ добавления новых элементов здесь очень похож на добавление элементов в очередь. Чтобы список был двусвязным, в строке 20 мы устанавливаем указатель, связывающий с предыдущим элементом, уже присутствующим в списке, либо создаём первый элемент, если список пуст. Эта часть позволяет обойтись без прохода по списку циклом for, как мы делали раньше. Тем не менее, это решение влияет на работу кода. В любом случае элементы помещаются в список по мере добавления, всегда в верхнюю часть списка.

Хорошо, добавление элементов в список один за другим не означает, что мы работаем ни в режиме FIFO, ни в режиме стека. Режим чтения определяется именно оператором, который мы реализовали в строке 29. В строке 31 мы немного корректируем поиск элемента, расположенного у основания списка. То есть само поведение определяется только тогда, когда вам нужно использовать эти элементы. Эта настройка гарантирует, что список будет читаться в режиме FIFO.

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

Что ж, думаю, до этого момента было нетрудно понять, что мы делаем. А теперь начинается сложная часть, и именно поэтому нам нужна настройка, которую мы сделали. Так что постарайтесь не терять нить моего объяснения. Иначе вы в какой-то момент совсем запутаетесь.

В предыдущих статьях я показал, как оператор индексации можно сочетать с оператором присваивания, чтобы изменять значение определённого элемента с помощью индекса, указывающего, какой именно элемент нужно изменить. Однако то, что мы рассмотрели в тех статьях, — лишь часть общей картины, и, между прочим, самая простая часть. Когда выполняется цикл в строке 43 кода 13, строка 44 фактически возвращает указатель на определённую область памяти. Именно по этой причине оператор присваивания, реализованный в строке 47, нуждается только в строке 49, чтобы работать и действительно изменять содержимое определённого элемента.

А теперь остановитесь и немного поразмышляйте над этим механизмом, уважаемый читатель. Если перегрузка оператора индексации, реализованная в строке 40, возвращает указатель на определённую область памяти, а в процедуре из строки 18 мы используем указатели, чтобы добавить в список новый элемент, то что мы получим, если применим тот же принцип к присваиванию значения с помощью оператора присваивания? Итак, именно ту реализацию, которая начинается в строке 52.

Хм, в некотором смысле этот механизм кажется мне довольно изящным. Должен признать, что, раз уж вы об этом заговорили, тот код 14 уже не кажется таким сложным. Мне нравится эта идея. Снова анализируя код 13, я вижу, что код, используемый для добавления новых элементов, не очень похож на тот, что реализован в строке 52. Я даже подумал, что они могут быть одинаковыми, но вижу, что между ними есть некоторые различия. Не могли бы вы подробнее объяснить эти различия? Да, уважаемый читатель. Давайте тогда разберёмся, почему оба кода различаются.

Начнём с того, что код, реализованный в строке 18, знает, что элемент ВСЕГДА, АБСОЛЮТНО ВСЕГДА добавляется в верхнюю часть списка. В отличие от него, код в строке 52 не знает точно, где будет добавлен элемент, поскольку его можно вставить в любую точку списка. Но вот что интересно: для этого кода из строки 52 элемент всегда, в некотором смысле, добавляется в основание списка.

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

Я не это имею в виду, уважаемый читатель. Вы понимаете не то, что я объясняю. Чтобы прояснить разницу, просмотрите приведённый ниже код.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_04.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo[2] <<= 35;
14.     demo[0] <<= 47;
15.     demo[0] <<= 110;
16.     demo.Debug(__LINE__);
17.     demo <<= 51;
18.     demo.Debug(__LINE__);
19. 
20.     for (char info; demo >> info;)
21.         Print(info);
22. };
23. //+------------------------------------------------------------------+

Код 15

При выполнении кода 15 мы получаем следующий результат:

Рисунок 05

Обратите внимание на то, что произошло. При выполнении строки 17 код выдал довольно странный результат. Тем не менее, до строки 15 всё работало отлично — именно благодаря тому, как код формировал список. Почему именно строка 17 привела к тому, что список стал совершенно странным, настолько, что перестал корректно сохраняться? Итак, чтобы понять причину, нам нужно изменить код 13, добавив то, что показано в следующем фрагменте.

                   .
                   .
                   .
51. //+----------------+
52.         void operator<<=(const T arg)
53.         {
54.             C_Demo <T>  *loc = new C_Demo <T>;
55.             
56.             (*loc).m_info = arg;
57.             (*loc).m_next = GetPointer(this);
58.             (*loc).m_prev = m_prev;
59.             if (m_prev != NULL) (*m_prev).m_next = loc;
60.             m_prev = loc;
61.             PrintFormat("%s %d :: 0x%06X ->> 0x%06X:[%d] <<- 0x%06X ?? (%d)", __FUNCTION__, __LINE__, m_prev, GetPointer(this), m_info, m_next, arg);
62.         }
63. //+----------------+
                   .
                   .
                   .

Фрагмент 01

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

Рисунок 06

Хорошо, теперь я могу попытаться объяснить, как работает этот код в строке 51 и почему он всегда указывает на начало локального списка. И, что ещё важнее, теперь вы можете понять, почему нам нужно использовать оператор индексации, прежде чем пытаться использовать этот оператор из строки 51.

В первых трёх строках рисунка 06 мы всегда указываем на какую-либо позицию в уже сформированном списке. Эта ссылка на позицию в списке позволяет добавлять любой новый элемент между уже существующими элементами. Очевидно, что значения 47 и 110 мы помещаем в самое начало уже сформированного списка. А вот посмотрите, что происходит, когда мы пытаемся добавить в список значение 51. В данном случае у нас возникает проблема. Она приводит к тому, что при отладочном выводе списка получается совершенно странная последовательность. Эта последовательность разрушает список, который поддерживал наш код.

"Хорошо, но я всё ещё не до конца понимаю этот момент, уважаемый автор. Для меня всё выглядит совершенно нормально". Хорошо, посмотрим, смогу ли я сделать это немного понятнее для вас, мой друг-читатель. Для этого достаточно взглянуть на рисунок 04. Обратите внимание, что наименьшее шестнадцатеричное значение, указанное там, — 0x200000. Причина в том, что значение 0x100000 НЕ МОЖЕТ ИСПОЛЬЗОВАТЬСЯ. Это ограничение было объяснено в предыдущих статьях. Ознакомьтесь с ними, чтобы узнать подробности. Таким образом, при попытке доступа к элементу со значением 51 из-за строки 17 кода 15 происходит попытка использовать именно адрес 0x100000 в качестве следующего адреса списка, что совершенно недопустимо. По этой причине список в итоге разрушается.

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

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T> class C_Demo
05. {
06.     private:
07. //+----------------+
08.         T           m_info;
09.         C_Demo <T>  *m_prev,
10.                     *m_next;
11. //+----------------+
12. #define def_Adjust(x) (m_prev = (m_prev == NULL ? x : ((*m_prev).m_prev != NULL ? (*m_prev).m_prev : m_prev)))
13. //+----------------+
14.         void Debug_Private(string fn, uint line)
15.         {
16.             Print("===== DEBUG [", fn, " :: ", line, "]=====");
17.             for (C_Demo <T> *loc = m_prev; (loc != NULL); loc = (*loc).m_next)
18.                 PrintFormat("0x%06X ->> 0x%06X <<- 0x%06X = [%d]", (*loc).m_prev, loc, (*loc).m_next, (*loc).m_info);
19.             Print("=================");
20.         }
21. //+----------------+
                   .
                   .
                   .
59. //+----------------+
60.         void operator<<=(const T arg)
61.         {
62.             C_Demo <T>  *loc = new C_Demo <T>;
63.             
64.             (*loc).m_info = arg;
65.             (*loc).m_next = GetPointer(this);
66.             (*loc).m_prev = m_prev;
67.             if (m_prev != NULL) (*m_prev).m_next = loc;
68.             m_prev = loc;
69.             Debug_Private(__FUNCTION__, __LINE__);
70.         }
71. //+----------------+
72.         void Debug(string fn, uint line)
73.         {
74.             m_prev = def_Adjust(m_prev);
75.             Debug_Private(fn, line);
76.         }
77. //+----------------+
78. #undef def_Adjust
79. //+----------------+
80. };
81. //+------------------------------------------------------------------+

Фрагмент 02

Здесь нас интересует именно выполнение строки 69. Во вложении вы найдёте этот заголовочный файл целиком. Так что вам не о чем беспокоиться, ведь это модификация кода 13. Данное изменение было необходимо, чтобы сохранить значение m_prev, потому что при выполнении строкой 69 исходного кода для отладки списка в итоге изменялось значение указателя m_prev. Причина сразу становится ясна. Итак, чтобы продемонстрировать это частичное построение списка, мы используем приведённый ниже код.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. #include "Include\C_Demo_04.mqh"
05. //+------------------------------------------------------------------+
06. void OnStart(void)
07. {
08.     C_Demo <char> demo;
09. 
10.     demo << 10;
11.     demo << 84;
12.     demo << -6;
13.     demo[2] <<= 35;
14.     demo[0] <<= 47;
15.     demo[4] <<= 110;
16.     demo.Debug(__FUNCTION__, __LINE__);
17. 
18.     for (char info; demo >> info;)
19.         Print(info);
20. };
21. //+------------------------------------------------------------------+

Код 16

Результат действительно интересен, уважаемый читатель. При выполнении этого кода 16 получается то, что показано ниже.

Рисунок 07

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


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

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

Удаление элементов из списка с помощью перегрузки операторов оказывается более сложной задачей. Не потому, что это сложно реализовать, а потому, что у большинства из вас, читателей, очень мало опыта; было бы несправедливо просто реализовать код удаления, не объяснив должным образом, почему он работает. По крайней мере, именно так чувствовал бы себя я, будь я на вашем месте, если бы кто-то просто дал мне код и оставил одного разбираться, как он работает.

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

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

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

Прикрепленные файлы |
Anexo.zip (5.46 KB)
Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (OpenCL примитивы) Нейросети в трейдинге: Адаптация прогноза при смене рыночного режима (OpenCL примитивы)
Статья продолжает практическую реализацию фреймворка OMPB для адаптации прогнозных моделей к смене рыночного режима. Рассматриваем вычислительную декомпозицию фреймворка и реализуем недостающие OpenCL-примитивы: ограниченный Disagreement, распространение градиента Mismatch и поэлементный расчёт KL-дивергенции для байесовского слоя. Полученные компоненты образуют параллельную GPU-основу без переноса больших промежуточных тензоров на CPU и подготавливают сборку полноценного OMPB-слоя в библиотеке нейронных сетей MQL5.
Разработка индикатора Volume Bubble на MQL5 с использованием стандартного отклонения Разработка индикатора Volume Bubble на MQL5 с использованием стандартного отклонения
В статье показано, как создать индикатор Volume Bubble на MQL5, который визуализирует рыночную активность с использованием статистической нормализации. В нем рассматривается работа с тиковым и реальным объемом, вычисление среднего значения и стандартного отклонения по скользящему окну, а также нормализация значений объема для определения относительной силы. Вы реализуете графические объекты для отображения пузырьков с динамическими размером и цветом, обеспечивая наглядное представление интенсивности торговой активности прямо на графике
Репликация и моделирование рынка: Большой финал Репликация и моделирование рынка: Большой финал
Я знаю, что многие, возможно, думали, что я опубликую ещё несколько статей, чтобы объяснить другие аспекты этой системы. Недостающие элементы легко реализовать. Тем не менее, её разработка позволит вам проверить, насколько вы действительно готовы.
Разработка инструментария для анализа Price Action (Часть 72): Создание индикатора заполнения гэпов на MQL5 Разработка инструментария для анализа Price Action (Часть 72): Создание индикатора заполнения гэпов на MQL5
Инструмент заполнения гэпов выходного дня для MetaTrader 5, готовый к использованию советниками: он обнаруживает гэпы, подтверждает их полное заполнение и записывает детерминированные значения Buy и Sell в буферы индикатора. Он восстанавливает исторические события, отслеживает рынок в реальном времени без перерисовки и визуализирует структуру гэпов непосредственно на графике. Настраиваемые алерты и наглядные графические объекты поддерживают как ручной анализ, так и автоматическое исполнение.