Del básico al intermedio: Sobrecarga de operadores (II)
Introducción
En el artículo anterior, Del básico al intermedio: Sobrecarga de operadores (I), comenzamos a hablar sobre cómo implementar lo que se conoce como sobrecarga de operadores. Sin embargo, aquel artículo tenía como único objetivo iniciar la presentación de uno de los conceptos más confusos para los principiantes. Esto se debe a que, según cómo se implemente la sobrecarga de operadores, un mismo código puede resultar más legible o mucho más confuso. Y todo porque el programador no tiene en cuenta que la sobrecarga de operadores debe utilizarse precisamente para volver el código más legible y fácil de comprender.
No obstante, aquel artículo fue solo una breve y agradable introducción a las posibilidades que ofrece la sobrecarga de operadores y a lo divertido e interesante que resulta este tema, tanto desde el punto de vista práctico como teórico. Quiero presentar las cosas de la forma más amena y sencilla posible. Y todo ello sin entrar en el tema que abordaré en otros cuatro artículos que publicaré, donde exploraré otra forma de aplicar la sobrecarga de operadores orientada a un tipo de aplicación muy específico. Pero esa es otra historia. Así que permanece atento a todos mis artículos, porque esta otra forma de aplicar la sobrecarga es realmente muy práctica y fácil de comprender. Sin embargo, no abordaré ese sistema aquí, en esta serie de artículos. Bien, pasemos entonces al tema principal de este artículo.
Sobrecarga de operadores (II)
Muy bien, quizá una de las tareas más importantes en programación sea depurar el código, ya sea mediante un IDE, como MetaEditor, o directamente a través de un archivo de registro. En algunos casos más sencillos, podemos utilizar el propio ToolBox de mensajes del terminal de MetaTrader 5. La forma de depurar el código importa poco. Sin embargo, conocer los mecanismos de depuración puede ayudarnos a encontrar fallos y errores que, de otro modo, serían muy difíciles de detectar.
Bien, si prestaste atención al artículo anterior, probablemente notaste que, casi al final, hice una pequeña broma en uno de los códigos fuente que incluí allí. Aquella broma pretendía precisamente demostrar algo que aquí abordaremos con mayor profundidad. Esto puede resultarte interesante a medida que estudies y quieras practicar la sobrecarga de operadores. Como quiero mantener todo lo más sencillo y didáctico posible, comenzaremos este artículo revisando uno de los códigos que analizamos en el artículo anterior. A partir de ahí, avanzaremos hacia lo que realmente quiero demostrar. A continuación te presento el código completo.
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. //+------------------------------------------------------------------+
Código 01
Bien. Ahora quiero que apartes cualquier cosa que pueda distraerte para que entiendas el procedimiento que seguiremos. Intentaré explicar algo que hace perder los estribos a muchas personas cuando se trata de depurar código. Sin embargo, comenzaremos con una implementación más sencilla y fácil de entender. Después te mostraré algo que roza lo absurdo, pero que puede ocurrir al depurar un código que utiliza la sobrecarga de operadores.
Muy bien, la broma está en la línea 47. Pregunta: ¿por qué funciona esta línea 47 del código 01? Respuesta: porque el compilador no la interpreta exactamente como aparece escrita. Una vez más, comprender lo explicado en un artículo es muy importante para entender todos los demás. En el fragmento siguiente te muestro cómo interpreta el compilador la línea 47 del código 01.
. . . c.operator+=(4).Debug(); . . .
Fragmento 01
Ahora, al analizar este fragmento 01, resulta perfectamente comprensible por qué la línea 47 del código 01 funciona e imprime algo en el terminal. ¿Verdad? Sin embargo, quiero mostrarte cómo ampliar la operación que ejecuta esa línea. Para ello, realizaremos un pequeño cambio en el código 01. Como la modificación es sencilla y directa, la presentaré mediante un fragmento, ya que no es necesario reproducir todo el código para explicar el cambio.
. . . 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. //+------------------------------------------------------------------+
Fragmento 02
Observa lo siguiente: este fragmento 02 recoge los cambios que introdujimos en el código 01 para implementar un mecanismo de depuración más adecuado para lo que quiero demostrar. Fíjate en que hemos definido el procedimiento Debug en la línea 31 para que reciba un argumento. Este valor indicará desde qué línea del código se invocó el procedimiento. Para ello, añadimos un pequeño detalle a las líneas que imprimen información en el terminal. Así, al ejecutar este nuevo código 01 con los cambios del fragmento 02, obtendremos el resultado de la imagen siguiente.

Imagen 01
Es decir, ya contamos con un mecanismo de depuración bastante útil. Perfecto. Ahora quiero trasladar la depuración de las líneas 45 y 49 a las líneas 44 y 48. Pregunta: ¿cómo podemos depurar directamente esas líneas? Hum, parece algo muy difícil y extremadamente complicado, ya que en ambas estamos asignando un valor a una variable. Por tanto, no veo cómo podríamos depurarlas.
Pues bien, mi querido lector, una vez más estás analizando las cosas de una manera cuando, en realidad, deberías interpretar esas líneas desde una perspectiva completamente diferente y menos convencional. Quiero que analices las líneas 44 y 48 como expresiones equivalentes a la del fragmento 01. Entonces vuelvo a preguntarte: ¿cómo podríamos depurar las líneas 44 y 48 e imprimir información en el terminal de MetaTrader 5?
Bien, existen dos formas de implementar esta depuración: una que prácticamente ya tenemos lista y otra que tendríamos que desarrollar. La opción que elijas dependerá del esfuerzo que estés dispuesto a dedicar a la implementación del código. Pero, antes de explicarte cómo implementar la depuración en ambas líneas, analicemos cómo las interpretaría el compilador. A partir del contenido del propio código 01, el compilador interpretaría esas líneas de la siguiente manera.
. . . c = a.operator+(b); . . . c = b.operator+(4); . . .
Fragmento 03
El fragmento 03 reproduce la interpretación que hace el compilador de esas líneas. Hum, interesante. Entonces, pensándolo bien, solo tendríamos que imaginar el fragmento 03 transformado en algo parecido al fragmento 01 para obtener la solución. ¿Sería esa la respuesta? Sí, mi querido lector. Ahora estás progresando. Pero hay un detalle: en lugar de escribir la expresión del fragmento 03 y añadir la del fragmento 01 para crear la línea de depuración, podemos formular la expresión de una manera más adecuada. Para ello, bastará con sustituir tanto la línea 44 como la 48 por la expresión que presento en el fragmento siguiente. Sin embargo, esto generará otro problema que impedirá compilar el código. Pero no te preocupes, te explicaré cómo corregirlo.
. . . 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. //+------------------------------------------------------------------+
Fragmento 04
Aunque la idea es básicamente la del fragmento, puedes darte cuenta enseguida de que quizá no funcione debido a un pequeño detalle del código 01. Si intentas compilar la nueva versión del código 01 después de aplicar los cambios del fragmento 04, el compilador emitirá algunos errores bastante extraños, que aparecen en la imagen siguiente.

Imagen 02
Los errores de la imagen 02 se deben a que la llamada utilizada para depurar la línea NO ES UNA FUNCIÓN, SINO UN PROCEDIMIENTO. Por eso el compilador informa de esos errores. Corregirlo es muy sencillo y directo, siempre que conozcas el mecanismo. No basta con ir a la línea 31 del código y convertir el procedimiento en una función, porque no funciona de esa manera. Para corregir el problema, la función debe devolver una referencia a un objeto de nuestra propia clase. Recuerda que esto implica devolver algo equivalente a lo que contienen las líneas 23 y 28.
Por tanto, debes analizar cómo interpretará el compilador este fragmento 04. A partir de ahí, podemos adoptar dos soluciones bastante sencillas. Te presento la primera a continuación.
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. //+------------------------------------------------------------------+
Código 02
Observa que ahora hemos modificado el funcionamiento de la línea 31. Así, la función devolverá un valor asignable a la variable c, como indican las líneas 45 y 48. Para ello, añadimos la línea 34, que crea el valor que la función devolverá al finalizar la depuración. Al ejecutar este código 02, obtendremos el resultado de la imagen siguiente.

Imagen 03
Esto sí que resulta bastante interesante. Pero podemos mejorar aún más la implementación. Cuando el programa ejecuta la línea 34 del código 02, la expresión invoca el constructor definido en la línea 14 y crea una nueva instancia temporal del tipo stComplex, definido como estructura. Sin embargo, en la práctica, difícilmente encontrarás una implementación así, ya que es completamente innecesaria. En la práctica, utilizamos otro operador para referirnos a la instancia actual de ese tipo. Como todavía no lo hemos utilizado, ha llegado el momento de que lo conozcas. Así que saluda al operador this.
El operador this permite sustituir precisamente la línea 34 del código 02. Sin embargo, y esta es la parte importante, esa línea invoca el constructor, crea una nueva instancia temporal de stComplex y devuelve su valor. En cambio, el operador this obtiene una referencia a la instancia actual, sin construir otra instancia. Sé que esto puede parecer algo confuso, pero en la práctica es mucho más sencillo. De hecho, el código 02 solo necesita una modificación, que presento en el fragmento siguiente.
. . . 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. //+----------------+ . . .
Fragmento 05
Bien, creo que ya entiendo cómo proceder. Pero tengo una duda bastante complicada. Has explicado que el operador this obtiene una referencia a la instancia actual de stComplex. Por eso, la implementación del fragmento 05 sería la más adecuada. Sin embargo, y esta es precisamente mi duda, no veo ninguna diferencia entre usar la línea 34 del código 02 y la misma línea del fragmento 05, ya que al final el resultado será exactamente el mismo.
Sí, mi querido lector, el resultado es exactamente el mismo, pero las direcciones no lo son. Cuando utilices la expresión de la línea 34 del código 02, en realidad estarás creando una nueva instancia temporal de stComplex, distinta de la instancia actual. Dependiendo de lo que estés implementando, crear una nueva instancia PODRÁ —y quiero destacar claramente este 'podrá'— generar una operación cuyo resultado sea completamente distinto del que esperabas, precisamente porque has creado otro objeto. Recuerda lo siguiente: como programador, JAMÁS debes crear algo cuyo resultado no puedas predecir. No tiene ningún sentido programar algo sin saber si el resultado es correcto o incorrecto. Sin embargo, al utilizar el operador this en la línea 34 del fragmento 05, NO ESTARÁS CREANDO una nueva instancia. Estarás accediendo a la instancia actual mediante la referencia que proporciona this y utilizando los valores almacenados en sus variables miembro.
Como ya expliqué, este tipo de cuestión puede resultar algo confusa. Sin embargo, no estoy aquí solo para explicar cómo funcionan las cosas o cómo deberían interpretarse. Estoy aquí para mostrarte cómo funcionan realmente entre bastidores. Para comprender si debemos utilizar o no el operador this en el código, tendremos que analizar la diferencia entre crear una nueva instancia y referirnos a la instancia actual. Para ello, utilizaremos el código más sencillo posible. Así, creo que quedará mucho más claro cómo una simple elección puede influir en todo tu código. Recuerda, una vez más, que esto puede ocurrir o no, dependiendo, por supuesto, de lo que estés implementando. Para cumplir este objetivo, te presento el siguiente código.
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. //+------------------------------------------------------------------+
Código 03
Al ejecutar este código 03, el terminal de MetaTrader 5 generará el resultado de la imagen siguiente.

Imagen 04
Ahora analiza los valores de la imagen 04. En la línea cuatro definimos la clase C_Demo, es decir, el tipo al que pertenecerán sus instancias; esta definición no crea ninguna instancia. En las líneas ocho y trece definimos dos funciones miembro. En la línea diez, la primera función utiliza el operador this para obtener una referencia a la instancia sobre la que se invocó la función y pasa esa referencia a GetPointer, que devuelve la dirección de memoria de esa instancia. La segunda función, definida en la línea trece, utiliza otra expresión: la expresión C_Demo() invoca el constructor, crea una nueva instancia temporal del tipo C_Demo y GetPointer devuelve la dirección de memoria de esa instancia temporal.
Observa que en la línea 24 declaramos la única variable de este código 03: a, de tipo C_Demo. Al ejecutar esa declaración, el programa construye una instancia de C_Demo y la almacena en la variable a; mediante esa variable invocamos las dos funciones miembro. Ahora viene la parte curiosa. En la línea 26 imprimimos la dirección que devuelve la función definida en la línea ocho, obtenida a partir de la referencia this a la instancia almacenada en a. En la línea 27 imprimimos directamente la dirección de memoria de esa misma instancia. Y en la línea 28 imprimimos la dirección que devuelve la función definida en la línea trece. Al comparar los resultados, puedes comprobar claramente que las dos primeras direcciones son iguales: this se refiere a la instancia almacenada en a, no a la definición de la clase ni a la variable como entidad independiente. La tercera dirección es distinta porque la expresión C_Demo() de la línea quince invoca el constructor, crea otra instancia temporal y GetPointer devuelve la dirección de esa nueva instancia.
Bien, teniendo en cuenta este resultado, creo que ha quedado bastante claro cuándo y cómo utilizar el operador this para referirse a la instancia sobre la que se invocó una función miembro. Después, GetPointer puede devolver la dirección de esa instancia. Esto es distinto de invocar el constructor, crear otra instancia del mismo tipo y devolver la dirección de ese nuevo objeto.
Sin embargo, quiero abordar una cuestión relacionada con la sobrecarga de operadores. Me gustaría que prestaras mucha atención al grado de sutileza de lo que analizaremos aquí, ya que, en determinadas circunstancias, puede generar resultados extremadamente extraños. Como el caso que presentaré puede resultar algo preocupante, te recomiendo estudiar el tema con calma y no sacar conclusiones precipitadas. Pero, para separar los temas, abramos un nuevo apartado.
Cuidado con la sobrecarga de operadores
Algo que muchas personas ignoran es que los programas pueden generar resultados extraños en situaciones concretas. Muchos creen que las computadoras siempre ofrecerán resultados correctos, siempre que estén programadas correctamente. Sin embargo, no siempre es así. Lo que analizaré aquí es una situación que difícilmente encontrarás documentada o reproducida. Se trata de una situación muy específica en la que un código a veces genera el resultado correcto y otras veces produce uno incorrecto. Y, por extraño que parezca, el código estará correctamente programado.
Bien, el código que utilizaremos es uno que conservo desde hace años, ya que lo creé originalmente en C++ mientras esperaba el momento adecuado para compartir este conocimiento con otros programadores. Al fin y al cabo, para comprender el análisis que presentaré es necesario conocer una serie de conceptos que tú, mi querido lector, probablemente ya dominas si has seguido esta serie de artículos.
A continuación te presento el código.
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. //+------------------------------------------------------------------+
Código 04
Ahora presta mucha atención. En principio, este código 04 tiene el mismo objetivo que los demás códigos que analizamos en este artículo. Sin embargo, contiene un problema que difícilmente podrás resolver si estás empezando a programar. Incluso si envías este código a otros programadores, tampoco encontrarán realmente ningún fallo. El código a veces produce un resultado correcto y otras veces uno incorrecto, dependiendo, por supuesto, de la operación que ejecutes. Para demostrarlo, analiza el siguiente resultado de la ejecución.

Imagen 05
La región resaltada es la que realmente nos interesa. Ahora te pregunto: ¿por qué los resultados son tan distintos si los valores utilizados son iguales y la operación realizada también es la misma? Puedes analizar la operación en las líneas 52 y 53 del código 04. Sin embargo, a pesar de tratarse del mismo tipo de operación y de utilizar los mismos valores, el resultado es completamente diferente.
Quizá estés pensando: «Vaya, este código no tiene ningún sentido. ¿Por qué utilizas esos paréntesis dobles en la línea 52? Tal vez eso sea lo que produce los resultados incorrectos». Bien, ese no es el problema, mi querido lector. No lo estás entendiendo. Pero intentaré explicarlo de otra manera. Quizá así resulte un poco más sencillo comprender dónde está el problema.
Si aplicamos al código 04 los cambios del fragmento siguiente, la situación empieza a cambiar.
. . . 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. //+------------------------------------------------------------------+
Fragmento 06
Ahora, al ejecutar el código con los cambios del fragmento 06, obtenemos el siguiente resultado.

Imagen 06
Una vez más, la región resaltada es la que nos interesa. Observa que el problema no son los paréntesis dobles. Están ahí por una razón. Lo que ocurre es que existe algún tipo de error extraño o alguna interacción muy poco habitual. Pero ¿por qué? Bien, quizá todavía no hayas comprendido lo extraño que resulta este fallo. Déjame explicártelo. Si analizas con atención el código 04, notarás que utilizamos una directiva en la línea 40 para inspeccionar los valores almacenados en la instancia. El código funciona cuando no evaluamos esa expresión de inspección, como en la línea 54. Sin embargo, al evaluarla en la línea 53 del fragmento 06, el programa produce un resultado incorrecto. La pregunta es: ¿por qué la expresión utilizada para inspeccionar esos valores altera el resultado en una línea hasta generar un valor completamente extraño y sin sentido, pero no lo altera en la otra, donde primero ejecutamos la operación y solo después inspeccionamos los valores almacenados en la variable? Este es el verdadero problema, y no cualquier otro que puedas estar imaginando.
Por tanto, este tipo de situación dificulta mucho la depuración: si no inspeccionamos la ejecución, el código parece funcionar perfectamente. Sin embargo, al evaluar la expresión utilizada para inspeccionar los valores, el código comienza a generar resultados extraños.
Pero, antes de profundizar en esta cuestión y explicarte qué solución debemos adoptar para evitar este tipo de problema, entendamos por qué necesitamos utilizar paréntesis dobles en las líneas 52 y 53 del fragmento 06.
Muy bien, el motivo es que, sin los paréntesis dobles, el compilador no puede formar una expresión válida. Pero ¿cómo es eso? No entiendo lo que quieres decir. Bien, volvamos al código 01, al comienzo del artículo. Allí expliqué por qué la línea 47 del código 01 necesita utilizar paréntesis. Lo mismo se aplica aquí, tanto en el código 04 como en el fragmento 06. Sin embargo, a diferencia de la interpretación del código 01, si no utilizáramos los paréntesis dobles en la línea 52 del código 04, el compilador generaría los siguientes errores.

Imagen 07
Esto ocurre porque, al procesar la definición de la línea 40 para formar la expresión de la línea 52, el compilador no puede completar una interpretación válida. Bien, si no sabes cómo interpreta el compilador las definiciones, consulta el artículo Del básico al intermedio: Definiciones (I). Bien, ya hemos explicado el motivo de los paréntesis dobles. Ahora falta identificar qué provoca que el código genere valores incorrectos. Para averiguarlo, debemos añadir algo al código 04. De este modo, obtendremos la siguiente versión.
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. //+------------------------------------------------------------------+
Código 05
En el código 05 añadimos, en la línea 21, un nuevo mensaje de depuración. La línea 21 imprimirá este mensaje en el terminal y nos permitirá comprobar cuántas veces el código llama a la función definida en la línea 19. Ahora presta atención a lo siguiente. Si todo ocurre como esperamos, la línea 21 imprimirá un mensaje ANTES de ejecutar la línea 54 y otro ANTES de ejecutar la línea 56. ¿Entendido? Bien, compilemos entonces este código 05 y analicemos la salida del terminal. Y, para sorpresa de todos, el terminal genera el resultado de la imagen siguiente.

Imagen 08
Pero qué cosa tan extraña. En un caso, la expresión evaluada provoca dos llamadas; en otro, solo una, como esperábamos. Vaya, ¿qué está pasando aquí? Definitivamente, no entiendo dónde está el error, porque esto no tiene ningún sentido. ¿Cómo puede el procesamiento de la expresión generar ese comportamiento? En efecto, mi querido lector, este tipo de fallo es muy extraño y extremadamente difícil de corregir, sobre todo cuando estamos aprendiendo o tenemos poca experiencia en programación. En ese caso, definitivamente no sabremos cómo proceder, ya que, a primera vista, el código no contiene ningún fallo. A veces funciona y otras veces no, y todo depende de si evaluamos o no la expresión de inspección en una determinada línea de código.
Sin embargo, a pesar de toda esta locura, la solución, por extraño que parezca, no está donde quizá imagines; es decir, no se trata de un fallo en el código que implementa el operador de la línea 19. En realidad, el fallo está en la definición utilizada para construir la expresión de inspección. ¿Qué? Un momento. ¿Cómo es eso? Lo que dices tiene todavía menos sentido. ¿Cómo puede estar el fallo en la definición si, al instrumentar el código con el cambio realizado en el código 05, comprobamos claramente que ocurre algo extraño: la línea 54 llama dos veces a la función definida en la línea 19? Eso es precisamente lo que muestra la imagen 08. No entiendo por qué el fallo está en la definición utilizada para construir la expresión de inspección.
Pero sí, mi querido lector, el fallo está en la definición. Aunque no exactamente en la definición en sí, sino en las expresiones de acceso a los datos que forma su expansión. Por algún extraño motivo, a veces el compilador procesa esa expansión de una forma distinta a la esperada. No entraré en detalles sobre ello para no volver aún más confuso algo que ya resulta confuso de por sí. Sin embargo, si redefinimos la directiva PrintX, corregiremos el problema analizado en este apartado. Para ello, modificaremos el código de la siguiente manera.
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. //+------------------------------------------------------------------+
Código 06
Ahora, al ejecutar este código 06, el terminal generará el resultado de la imagen siguiente.

Imagen 09
Esto demuestra que, en efecto, el fallo ha sido corregido.
Consideraciones finales
Ha sido bastante divertido escribir este artículo, aunque lo que hemos analizado pueda parecer muy confuso y difícil de comprender. Sin embargo, me gustaría que estudiaras y comprendieras muy bien lo que he explicado aquí, mi querido lector, porque algún día podría ocurrirte algo parecido. Y, sin la experiencia necesaria, seguramente acabarás bastante frustrado intentando comprender por qué tu código genera unas veces un resultado y otras veces otro.
No obstante, creo haber transmitido parte de mis conocimientos sobre este tema, ya que gran parte de lo que he expuesto aquí es fruto de muchos años analizando situaciones extrañas y teniendo que encontrar siempre una solución. Pero, si solo uno de ustedes asimila lo que he explicado aquí, habrá merecido la pena escribir este artículo.
| Archivo MQ5 | Descripción |
|---|---|
| Código 01 | Demostración básica |
| Código 02 | Demostración básica |
| Código 03 | Demostración básica |
| Código 04 | Demostración básica |
| Código 05 | Demostración básica |
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/16903
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Simulación de mercado: La unión hace la fuerza (I)
Simulación de mercado: Position View (XX)
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 24): Herramienta de cuantificación de la acción del precio
Del básico al intermedio: Sobrecarga de operadores (I)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso