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

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

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

Введение

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

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


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

Итак, пожалуй, одной из самых важных задач в программировании является отладка кода — будь то с помощью IDE, такой как MetaEditor, или напрямую через файл журнала. В некоторых более простых случаях можно воспользоваться самим окном сообщений ToolBox в терминале MetaTrader 5. Способ отладки кода не так уж важен. Тем не менее знание механизмов отладки может помочь нам обнаружить сбои и ошибки, которые в противном случае было бы очень трудно выявить.

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

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

Код 01

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

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

                  .
                  .
                  .
    c.operator+=(4).Debug();
                  .
                  .
                  .

Фрагмент 01

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

                   .
                   .
                   .
30. //+----------------+
31.         void Debug(uint arg)
32.         {
33.             PrintFormat("Debugging the line %d = %.02f %c %.02fi", arg, m_r, (m_i < 0 ? '-' : '+'), MathAbs(m_i));
34.         }
35. //+----------------+
36. };
37. //+------------------------------------------------------------------+
38. void OnStart(void)
39. {
40.     stComplex   a(2, 5),
41.                 b(8, -3),
42.                 c;
43. 
44.     c = a + b;
45.     c.Debug(__LINE__);
46. 
47.     (c += 4).Debug(__LINE__);
48.     c = b + 4;
49.     c.Debug(__LINE__);
50. }
51. //+------------------------------------------------------------------+

Фрагмент 02

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

Изображение 01

То есть теперь у нас уже есть довольно полезный механизм отладки. Отлично. Теперь я хочу перенести отладку со строк 45 и 49 непосредственно на строки 44 и 48. Вопрос: как мы можем отлаживать эти строки напрямую? Хм, это кажется очень трудным и чрезвычайно сложным, ведь в обоих случаях мы присваиваем значение переменной. Поэтому я не вижу, как мы могли бы их отлаживать.

Что ж, уважаемый читатель, вы в очередной раз смотрите на вещи под одним углом, тогда как на самом деле вам следует рассматривать эти строки с совершенно иной, менее традиционной точки зрения. Я хочу, чтобы вы рассматривали строки 44 и 48 как выражения, эквивалентные выражению из фрагмента 01. Тогда я снова спрашиваю вас: как мы могли бы отладить строки 44 и 48 и вывести информацию в терминал MetaTrader 5?

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

                   .
                   .
                   .
    c = a.operator+(b);
                   .
                   .
                   .
    c = b.operator+(4);
                   .
                   .
                   .

Фрагмент 03

Фрагмент 03 воспроизводит то, как компилятор интерпретирует эти строки. Хм, интересно. Итак, если хорошенько подумать, нам нужно лишь представить, что фрагмент 03 преобразован во что-то похожее на фрагмент 01, чтобы получить решение. Неужели это и есть ответ? Да, уважаемый читатель. Вот теперь вы движетесь в правильном направлении. Но есть одна деталь: вместо того чтобы записывать выражение из фрагмента 03 и добавлять к нему выражение из фрагмента 01, создавая строку отладки, мы можем сформулировать его более подходящим образом. Для этого достаточно заменить и строку 44, и строку 48 выражением, которое я привожу в следующем фрагменте. Однако это создаст другую проблему, которая помешает скомпилировать код. Но не волнуйтесь, я объясню, как это исправить.

                   .
                   .
                   .
37. //+------------------------------------------------------------------+
38. void OnStart(void)
39. {
40.     stComplex   a(2, 5),
41.                 b(8, -3),
42.                 c;
43. 
44.     c = (a + b).Debug(__LINE__);
45.     c.Debug(__LINE__);
46. 
47.     (c += 4).Debug(__LINE__);
48.     c = (b + 4).Debug(__LINE__);
49.     c.Debug(__LINE__);
50. }
51. //+------------------------------------------------------------------+

Фрагмент 04

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

Изображение 02

Ошибки на изображении 02 связаны с тем, что вызов, используемый для отладки строки, НЕ ЯВЛЯЕТСЯ ФУНКЦИЕЙ, А ЯВЛЯЕТСЯ ПРОЦЕДУРОЙ. Именно поэтому компилятор сообщает об этих ошибках. Решение здесь довольно простое, если понимать сам механизм. Недостаточно просто перейти к строке 31 кода и преобразовать процедуру в функцию, потому что это работает не так. Чтобы исправить проблему, функция должна возвращать ссылку на объект нашего собственного класса. Помните, что это подразумевает возврат чего-то эквивалентного тому, что содержится в строках 23 и 28.

Поэтому вам следует проанализировать, как компилятор интерпретирует этот фрагмент 04. Исходя из этого, мы можем выбрать два довольно простых решения. Ниже я представлю вам первое из них.

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

Код 02

Обратите внимание, что теперь мы изменили работу строки 31. Таким образом, функция вернёт значение, которое можно присвоить переменной c, как показано в строках 45 и 48. Для этого мы добавляем строку 34, которая создаёт значение, возвращаемое функцией по завершении отладки. При выполнении кода 02 мы получим результат, показанный на следующем изображении.

Изображение 03

А вот это уже довольно интересно. Но мы можем ещё улучшить реализацию. Когда программа выполняет строку 34 кода 02, это выражение вызывает конструктор, определённый в строке 14, и создаёт новый временный экземпляр типа stComplex, определённого как структура. Однако на практике вы вряд ли встретите такую реализацию, поскольку она совершенно не нужна. На практике мы используем другой оператор, чтобы ссылаться на текущий экземпляр этого типа. Поскольку мы его ещё не использовали, пришло время с ним познакомиться. Так что познакомьтесь с оператором this.

Оператор this позволяет заменить именно строку 34 кода 02. Однако — и это самое важное — эта строка вызывает конструктор, создаёт новый временный экземпляр stComplex и возвращает его значение. Вместо этого оператор this позволяет обратиться к текущему экземпляру без создания нового временного объекта. Я знаю, что это может показаться немного запутанным, но на практике всё гораздо проще. На самом деле коду 02 нужна всего одна правка, которую я покажу в следующем фрагменте.

                   .
                   .
                   .
30. //+----------------+
31.         stComplex Debug(uint arg)
32.         {
33.             PrintFormat("Debugging the line %d = %.02f %c %.02fi", arg, m_r, (m_i < 0 ? '-' : '+'), MathAbs(m_i));
34.             return this;
35.         }
36. //+----------------+
                   .
                   .
                   .

Фрагмент 05

Хорошо, кажется, теперь я понимаю, как действовать. Но у меня есть довольно непростой вопрос. Вы объяснили, что оператор this получает ссылку на текущий экземпляр stComplex. Поэтому реализация из фрагмента 05 была бы наиболее подходящей. Однако — и именно в этом заключается мой вопрос — я не вижу никакой разницы между использованием строки 34 из кода 02 и той же строки из фрагмента 05, поскольку в итоге результат будет точно таким же.

Да, мой дорогой читатель, результат будет точно таким же, но адреса будут разными. Когда вы используете выражение из строки 34 кода 02, вы фактически создаёте новый временный экземпляр stComplex, отличный от текущего экземпляра. В зависимости от того, что вы реализуете, создание нового экземпляра МОЖЕТ — и я хочу особо подчеркнуть это «может» — привести к операции, результат которой будет совершенно иным, чем вы ожидали, именно потому, что вы создали другой объект. Запомните: как программист, вы НИКОГДА не должны создавать то, результат чего не можете предсказать. Нет никакого смысла программировать что-либо, не зная, будет ли результат правильным или нет. Однако при использовании оператора this в строке 34 фрагмента 05 вы НЕ БУДЕТЕ СОЗДАВАТЬ новый экземпляр. Вы будете обращаться к текущему экземпляру через ссылку, которую предоставляет this, и использовать значения, хранящиеся в его переменных-членах.

Как я уже объяснял, подобные вещи могут казаться немного запутанными. Однако я здесь не только для того, чтобы объяснить, как всё устроено или как это следует интерпретировать. Я здесь, чтобы показать вам, как всё на самом деле работает за кулисами. Чтобы понять, следует ли использовать оператор this в коде, нам нужно проанализировать разницу между созданием нового экземпляра и обращением к текущему экземпляру. Для этого мы будем использовать как можно более простой код. Так, думаю, станет гораздо понятнее, как даже один такой выбор может повлиять на весь код. Помните, ещё раз, что это может произойти, а может и нет — в зависимости, конечно, от того, что именно вы реализуете. Чтобы достичь этой цели, предлагаю вам следующий код.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. class C_Demo
05. {
06.     public  :
07. //+----------------+
08.         C_Demo *Check_1(void)
09.         {
10.             return GetPointer(this);
11.         }
12. //+----------------+
13.         C_Demo *Check_2(void)
14.         {
15.             return GetPointer(C_Demo());
16.         }
17. //+----------------+
18. };
19. //+------------------------------------------------------------------+
20. #define PrintX(x) PrintFormat("0x%08X -> %s", x, #x)
21. //+------------------------------------------------------------------+
22. void OnStart(void)
23. {
24.     C_Demo a;
25. 
26.     PrintX(a.Check_1());
27.     PrintX(GetPointer(a));
28.     PrintX(a.Check_2());
29. }
30. //+------------------------------------------------------------------+

Код 03

При выполнении этого кода 03 терминал MetaTrader 5 выдаст результат, показанный на следующем изображении.

Изображение 04

Теперь проанализируйте значения на изображении 04. В строке 4 мы определяем класс C_Demo, то есть тип, к которому будут принадлежать его экземпляры; это определение не создаёт никаких экземпляров. В строках 8 и 13 мы определяем две функции-члены. В строке 10 первая функция использует оператор `this` для получения ссылки на экземпляр, для которого была вызвана функция, и передаёт эту ссылку в `GetPointer`, который возвращает адрес памяти этого экземпляра. Вторая функция, определённая в строке 13, использует другое выражение: выражение C_Demo() вызывает конструктор, создаёт новый временный экземпляр типа C_Demo, а `GetPointer` возвращает адрес памяти этого временного экземпляра.

Обратите внимание, что в строке 24 мы объявляем единственную переменную в этом коде 03: a, типа C_Demo. При выполнении этого объявления программа создаёт экземпляр класса C_Demo и сохраняет его в переменной a; с помощью этой переменной мы вызываем обе функции-члены. Теперь начинается самое интересное. В строке 26 мы выводим адрес, возвращаемый функцией, определённой в строке 8, полученный на основе ссылки `this` на экземпляр, хранящийся в `a`. В строке 27 мы напрямую выводим адрес памяти этого же экземпляра. А в строке 28 мы выводим адрес, который возвращает функция, определённая в строке 13. Сравнив результаты, можно ясно увидеть, что первые два адреса совпадают: `this` относится к экземпляру, хранящемуся в `a`, а не к определению класса и не к переменной как самостоятельной сущности. Третий адрес отличается, потому что выражение C_Demo() в строке 15 вызывает конструктор, создаёт ещё один временный экземпляр, а GetPointer возвращает адрес этого нового экземпляра.

Итак, учитывая этот результат, думаю, уже стало вполне ясно, когда и как следует использовать оператор `this`, чтобы ссылаться на экземпляр, для которого была вызвана функция-член. Затем GetPointer может вернуть адрес этого экземпляра. Это не то же самое, что вызвать конструктор, создать другой экземпляр того же типа и вернуть адрес этого нового объекта.

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


Осторожно с перегрузкой операторов

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

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

Ниже я привожу код.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     private:
07. //+----------------+
08.         double re;
09.         double im;
10. //+----------------+
11.     public  :
12. //+----------------+
13.         stComplex() :re(0), im(0) {} 
14. //+----------------+
15.         stComplex(const stComplex &o) :re(o.re), im(o.im) {}
16. //+----------------+
17.         stComplex(const double r, const double i) :re(r), im(i) {} 
18. //+----------------+
19.         stComplex operator+=(const stComplex &arg)
20.         {
21.             this = this + arg;            
22.             return this;
23.         }
24. //+----------------+
25.         stComplex operator+(const stComplex &arg)
26.         {
27.             stComplex res;
28. 
29.             res.re = this.re + arg.re;
30.             res.im = this.im + arg.im;
31. 
32.             return res;
33.         };
34.         double GetReal(void) const { return re; }
35. //+----------------+
36.         double GetImaginary(void) const { return im; }
37. //+----------------+
38. };
39. //+------------------------------------------------------------------+
40. #define PrintX(x) Print(__LINE__, " :: ", #x, " -> ", x.GetReal(), " ", x.GetImaginary(), "i")
41. //+------------------------------------------------------------------+
42. void OnStart(void)
43. {
44.     stComplex   a(2, 5),
45.                 b(8, -3),
46.                 c;
47. 
48.     c = a;
49.     PrintX(a);
50.     PrintX(b);
51.     PrintX(c);
52.     PrintX((a += b));
53.     c += b;
54.     PrintX(c);
55. }
56. //+------------------------------------------------------------------+

Код 04

А теперь будьте очень внимательны. В принципе, у этого кода 04 та же цель, что и у остальных кодов, которые мы разбираем в этой статье. Однако в нём есть проблема, которую вам вряд ли удастся решить, если вы только начинаете программировать. Даже если вы покажете этот код другим программистам, они тоже вряд ли найдут в нём какую-либо ошибку. Иногда код выдаёт правильный результат, а иногда — неправильный, в зависимости, конечно, от того, какую операцию вы выполняете. Чтобы убедиться в этом, проанализируйте приведённый ниже результат выполнения.

Изображение 05

Именно выделенная область нас действительно интересует. А теперь я спрошу вас: почему результаты так сильно различаются, если используемые значения одинаковы и выполняемая операция тоже одна и та же? Вы можете проанализировать эту операцию в строках 52 и 53 кода 04. Однако, несмотря на то что это один и тот же тип операции и используются одни и те же значения, результат получается совершенно другим.

Возможно, вы думаете: «Хм, этот код вообще не имеет никакого смысла. Почему вы используете эти двойные скобки в строке 52? Возможно, именно из-за этого и получаются неверные результаты». Что ж, проблема не в этом, мой дорогой читатель. Вы просто не понимаете, в чём дело. Но я постараюсь объяснить это по-другому. Возможно, так будет немного легче понять, в чём проблема.

Если внести в код 04 изменения из следующего фрагмента, ситуация начинает меняться.

                   .
                   .
                   .
41. //+------------------------------------------------------------------+
42. void OnStart(void)
43. {
44.     stComplex   a(2, 5),
45.                 b(8, -3),
46.                 c;
47. 
48.     c = a;
49.     PrintX(a);
50.     PrintX(b);
51.     PrintX(c);
52.     PrintX((a + b));
53.     PrintX((a += b));
54.     c += b;
55.     PrintX(c);
56. }
57. //+------------------------------------------------------------------+

Фрагмент 06

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

Изображение 06

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

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

Но прежде чем углубляться в этот вопрос и объяснять, какое решение следует принять, чтобы избежать подобных проблем, давайте разберёмся, почему в строках 52 и 53 фрагмента 06 необходимо использовать двойные скобки.

Хорошо, дело в том, что без двойных скобок компилятор не может сформировать корректное выражение. Но как такое возможно? Я не понимаю, что вы хотите сказать. Хорошо, вернёмся к коду 01 в начале статьи. Там я объяснил, почему в строке 47 кода 01 нужно использовать скобки. То же самое относится и к этому случаю — и к коду 04, и к фрагменту 06. Однако, в отличие от кода 01, если бы мы не использовали двойные скобки в строке 52 кода 04, компилятор выдал бы следующие ошибки.

Изображение 07

Это происходит потому, что при обработке определения из строки 40 для формирования выражения в строке 52 компилятор не может получить корректную интерпретацию. Что ж, если вы не знаете, как компилятор интерпретирует определения, ознакомьтесь со статьёй «От базового к среднему уровню: определения (I)». Итак, мы уже объяснили, зачем нужны двойные скобки. Теперь осталось выяснить, что именно заставляет код выдавать неверные значения. Чтобы это выяснить, нам нужно кое-что добавить в код 04. Таким образом, мы получим следующую версию.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     private:
07. //+----------------+
08.         double re;
09.         double im;
10. //+----------------+
11.     public  :
12. //+----------------+
13.         stComplex() :re(0), im(0) {} 
14. //+----------------+
15.         stComplex(const stComplex &o) :re(o.re), im(o.im) {}
16. //+----------------+
17.         stComplex(const double r, const double i) :re(r), im(i) {} 
18. //+----------------+
19.         stComplex operator+=(const stComplex &arg)
20.         {
21.             Print(__FUNCTION__);
22.             this = this + arg;            
23.             return this;
24.         }
25. //+----------------+
26.         stComplex operator+(const stComplex &arg)
27.         {
28.             stComplex res;
29. 
30.             res.re = this.re + arg.re;
31.             res.im = this.im + arg.im;
32. 
33.             return res;
34.         };
35.         double GetReal(void) const { return re; }
36. //+----------------+
37.         double GetImaginary(void) const { return im; }
38. //+----------------+
39. };
40. //+------------------------------------------------------------------+
41. #define PrintX(x) Print(__LINE__, " :: ", #x, " -> ", x.GetReal(), " ", x.GetImaginary(), "i")
42. //+------------------------------------------------------------------+
43. void OnStart(void)
44. {
45.     stComplex   a(2, 5),
46.                 b(8, -3),
47.                 c;
48. 
49.     c = a;
50.     PrintX(a);
51.     PrintX(b);
52.     PrintX(c);
53.     PrintX((a + b));
54.     PrintX((a += b));
55.     c += b;
56.     PrintX(c);
57. }
58. //+------------------------------------------------------------------+

Код 05

В коде 05 мы добавили в строке 21 новое сообщение отладки. Строка 21 выведет это сообщение в терминал и позволит нам проверить, сколько раз код вызывает функцию, определённую в строке 19. А теперь обратите внимание на следующее. Если всё произойдёт так, как мы ожидаем, строка 21 выведет одно сообщение ДО выполнения строки 54 и ещё одно — ДО выполнения строки 56. Понятно? Хорошо, тогда давайте скомпилируем этот код 05 и проанализируем вывод в терминале. И, ко всеобщему удивлению, терминал выдаёт результат, показанный на следующем изображении.

Изображение 08

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

Однако, несмотря на всё это безумие, решение, как ни странно, не там, где вы, возможно, думаете; то есть дело не в ошибке в коде, реализующем оператор в строке 19. На самом деле ошибка заключается в определении, используемом для построения выражения инспекции. Что? Подождите минутку. Как это так? То, что вы говорите, имеет ещё меньше смысла. Как ошибка может быть в определении, если при инструментировании кода изменением, внесённым в код 05, мы ясно видим, что происходит нечто странное: строка 54 дважды вызывает функцию, определённую в строке 19? Именно это и показано на изображении 08. Я не понимаю, почему ошибка заключается в определении, используемом для построения выражения инспекции.

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

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. struct stComplex
05. {
06.     private:
07. //+----------------+
08.         double re;
09.         double im;
10. //+----------------+
11.     public  :
12. //+----------------+
13.         stComplex() :re(0), im(0) {} 
14. //+----------------+
15.         stComplex(const stComplex &o) :re(o.re), im(o.im) {}
16. //+----------------+
17.         stComplex(const double r, const double i) :re(r), im(i) {} 
18. //+----------------+
19.         stComplex operator+=(const stComplex &arg)
20.         {
21.             this = this + arg;            
22.             return this;
23.         }
24. //+----------------+
25.         stComplex operator+(const stComplex &arg)
26.         {
27.             stComplex res;
28. 
29.             res.re = this.re + arg.re;
30.             res.im = this.im + arg.im;
31. 
32.             return res;
33.         };
34. //+----------------+
35.         void GetInfos(double &r, double &i) { r = re; i = im; }
36. //+----------------+
37. };
38. //+------------------------------------------------------------------+
39. #define PrintX(x) { double r, i; x.GetInfos(r, i); Print(__LINE__, " :: ", #x, " -> ", r, " ", i, "i"); }
40. //+------------------------------------------------------------------+
41. void OnStart(void)
42. {
43.     stComplex   a(2, 5),
44.                 b(8, -3),
45.                 c;
46. 
47.     c = a;
48.     PrintX(a);
49.     PrintX(b);
50.     PrintX(c);
51.     PrintX((a + b));
52.     PrintX((a += b));
53.     c += b;
54.     PrintX(c);
55. }
56. //+------------------------------------------------------------------+

Код 06

Теперь, при выполнении кода 06, терминал выведет результат, показанный на следующем изображении.

Изображение 09

Это доказывает, что ошибка действительно была исправлена.


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

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

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

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

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

Прикрепленные файлы |
Anexo.zip (2.8 KB)
Реализация шаблона "Декоратор" в MQL5: Добавление логирования, измерения времени выполнения и фильтрации к любому индикатору без изменения исходного кода Реализация шаблона "Декоратор" в MQL5: Добавление логирования, измерения времени выполнения и фильтрации к любому индикатору без изменения исходного кода
Сквозные аспекты, такие как логирование, измерение времени и пороговая фильтрация, не должны находиться внутри классов индикаторов. В данной статье показано, как применить шаблон проектирования "Декоратор" в MQL5 с общим интерфейсом IIndicator, базовым декоратором-владельцем CBaseDecorator и конкретными слоями-декораторами CLoggingDecorator, CTimingDecorator и CThresholdFilterDecorator. Для каждого советника можно комбинировать поведение декораторов, оставлять вычислительный код закрытым для изменений и обеспечивать детерминированную очистку удалением только внешнего декоратора.
Возможности Мастера MQL5, которые вам нужно знать (Часть 96): Использование вейвлетного порогового преобразования и сети LSTM в пользовательском классе управления капиталом Возможности Мастера MQL5, которые вам нужно знать (Часть 96): Использование вейвлетного порогового преобразования и сети LSTM в пользовательском классе управления капиталом
В этой статье рассматривается пользовательский класс Мастера MQL5 для управления капиталом. Пользовательский класс CMoneyWaveletLSTM создается объединением алгоритма вейвлетного порогового преобразования и сети LSTM. Как и в других статьях серии, разработанную модель можно тестировать с советниками, собранными в Мастере MQL5 и настраиваемыми с использованием различных классов трейлинг-стопов и входных сигналов. Как и в предыдущих статьях, в качестве сигнала входа используются встроенный класс Envelopes и класс RSI.
Моделирование рынка: Position View (XX) Моделирование рынка: Position View (XX)
В этой статье мы рассмотрим, как изменить код индикатора позиции, чтобы создать своего рода тень, которая позволит нам увидеть, где в данный момент находится цена, всё ещё актуальная на торговом сервере. Этот механизм призван облегчить планирование операций, в ходе которых мы перемещаем уровни стоп-лосса или тейк-профита. Добавление этой функции, то есть ценовых теней, может показаться чрезвычайно сложной задачей. В этой статье я покажу, что реализовать это можно очень просто и на практике.
Разработка индикатора Market Memory Zones: Зоны вероятного возврата цены Разработка индикатора Market Memory Zones: Зоны вероятного возврата цены
В рамках данного обсуждения мы разработаем индикатор для выявления ценовых зон, сформированных в результате высокой рыночной активности, например: импульсных движений, структурных сдвигов и выбросов ликвидности. Эти зоны представляют собой области, где рынок оставил «память» из-за неисполненных ордеров или быстрого изменения цен. Отмечая эти области на графике, индикатор показывает, где цена со статистически более высокой вероятностью вернется и отреагирует в будущем.