English Русский Español Deutsch 日本語 Português
preview
从基础到中级:模板和类型名称 (五)

从基础到中级:模板和类型名称 (五)

MetaTrader 5示例 |
343 0
CODE X
CODE X

概述

在上一篇文章,从基础到中级:模板和类型名称(四)中,我(尽可能清晰简洁地)解释了如何创建一个模板来概括一种建模类型,从而有效地创建可以被视为数据类型重载的东西。然而,在那篇文章的最后,我介绍了一些对许多读者来说可能很难掌握的东西:将数据传输到本身作为模板实现的函数或过程中。因为这个概念需要更详细的解释,我决定将这篇文章专门用于这个主题。此外,还有另一个与之密切相关的概念:它可以决定是否能够使用模板实现给定的解决方案。

因此,为了正确地开始这篇文章,让我们开始一个新的主题来解释为什么上一篇文章中的最后一段代码实际上是有效的。


拓展思维

在上一篇文章中,我们实现了一个看起来相当不寻常的东西。我怀疑你们中的许多人从未见过这样的东西。为了正确解释这一切意味着什么,我们需要回顾所使用的代码。你可以在下面看到。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T>
05. union un_01
06. {
07.     T value;
08.     uchar u8_bits[sizeof(T)];
09. };
10. //+------------------------------------------------------------------+
11. void OnStart(void)
12. {
13.     {
14.         un_01 <ulong> info;
15. 
16.         info.value = 0xA1B2C3D4E5F6789A;
17.         PrintFormat("The region is composed of %d bytes", sizeof(info));
18.         PrintFormat("Before modification: 0x%I64X", info.value);
19.         Swap(info);
20.         PrintFormat("After modification : 0x%I64X", info.value);
21.     }
22.     
23.     {
24.         un_01 <ushort> info;
25. 
26.         info.value = 0xCADA;
27.         PrintFormat("The region is composed of %d bytes", sizeof(info));
28.         PrintFormat("Before modification: 0x%I64X", info.value);
29.         Swap(info);
30.         PrintFormat("After modification : 0x%I64X", info.value);
31.     }
32. }
33. //+------------------------------------------------------------------+
34. template <typename T>
35. void Swap(un_01 <T> &arg)
36. {
37.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
38.     {
39.         tmp = arg.u8_bits[i];
40.         arg.u8_bits[i] = arg.u8_bits[j];
41.         arg.u8_bits[j] = tmp;
42.     }
43. }
44. //+------------------------------------------------------------------+

代码 01

现在,考虑到您可能已经对上一篇文章中的代码进行了相当多的实验,那么这段代码 01 一定引起了您的注意。特别是第 35 行所述过程的表述方式。为了理解代码 01 中以这种方式声明的内容,更重要的是,为什么要这样声明,我们需要仔细研究。

首先,我们需要了解第 35 行的过程是编译器在生成可执行代码的过程中会重载的过程。正如上一篇文章中提到的,我提出了一个挑战,即创建一段代码,执行与代码 01 相同的任务,但使用基于模板的重载来实现第 35 行中实现的过程。这项工作的目的是澄清为什么需要以这种特定方式声明过程。

理论上,这个练习似乎很简单,但在实践中,它可能有点棘手。那么,让我们一起来解决这个问题。这样,您将理解为什么声明必须按照代码 01 中的方式编写。

我们不会修改整个代码,只会修改过程,因此我们可以专注于下面显示的片段。

                   .
                   .
                   .
33. //+------------------------------------------------------------------+
34. void Swap(un_01 <ulong> &arg)
35. {
36.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
37.     {
38.         tmp = arg.u8_bits[i];
39.         arg.u8_bits[i] = arg.u8_bits[j];
40.         arg.u8_bits[j] = tmp;
41.     }
42. }
43. //+------------------------------------------------------------------+
44. void Swap(un_01 <ushort> &arg)
45. {
46.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
47.     {
48.         tmp = arg.u8_bits[i];
49.         arg.u8_bits[i] = arg.u8_bits[j];
50.         arg.u8_bits[j] = tmp;
51.     }
52. }
53. //+------------------------------------------------------------------+

片段 01

此片段 01 替换了第 35 行中的过程。但请注意:片段 01 中包含的内容正是编译器在翻译和重载代码 01 中看到的过程时所生成的内容。然而,在这种情况下,只涵盖了 ulong 和 ushort 类型,这与涵盖所有原始类型的代码 01 不同。

那么,为什么我们需要像片段 01 中那样声明事物呢?我相信现在原因应该很清楚了。如果你理解了上一篇文章中的解释,你可能会明白为什么代码 01 中的第 14 行和第 24 行必须以这种特定的方式声明。出于同样的原因,片段 01 中的第 34 行和第 44 行也需要遵循类似的结构。

记得我们在早期文章中讨论过通过值或引用传递的问题:声明变量时,必须同时指定其类型和名称。在函数或过程声明中,我们确实声明了一个变量,该变量可以是常数,也可以不是常数,具体取决于情况。

好吧,就过程中的变量声明而言,事情似乎相对简单,但还有一个问题:早些时候,当我们谈到特殊变量时,我们提到函数可以是其中之一。在我们现在处理的情况下,我们如何使用函数来实现我们需要的功能?

亲爱的读者,这是一个很好的问题。事实上,当我们想要返回值时,我们需要使用与用于过程不同的声明方式。然而,其基本概念与我们迄今为止所看到的非常相似。记住:当我们返回某个值时,该返回值本身就是一种变量,就像我们声明了一个名为函数名的变量一样。考虑到这一点,我们可以扩展相同的基本概念并正确实现我们的代码。

在进入一般形式之前,让我们先来看一个类似于片段 01 的方法。然而,由于函数的行为与过程不同,我们还将对代码 01 进行一些更改。结果如下所示。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T>
05. union un_01
06. {
07.     T value;
08.     uchar u8_bits[sizeof(T)];
09. };
10. //+------------------------------------------------------------------+
11. void OnStart(void)
12. {
13.     {
14.         un_01 <ulong> info;
15. 
16.         info.value = 0xA1B2C3D4E5F6789A;
17.         PrintFormat("The region is composed of %d bytes", sizeof(info));
18.         PrintFormat("Before modification: 0x%I64X", info.value);
19.         PrintFormat("After modification : 0x%I64X", Swap(info).value);
20.     }
21.     
22.     {
23.         un_01 <ushort> info;
24. 
25.         info.value = 0xCADA;
26.         PrintFormat("The region is composed of %d bytes", sizeof(info));
27.         PrintFormat("Before modification: 0x%I64X", info.value);
28.         PrintFormat("After modification : 0x%I64X", Swap(info).value);
29.     }
30. }
31. //+------------------------------------------------------------------+
32. un_01 <ulong> Swap(const un_01 <ulong> &arg)
33. {
34.     un_01 <ulong> local;
35. 
36.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
37.     {
38.         tmp = arg.u8_bits[i];
39.         local.u8_bits[i] = arg.u8_bits[j];
40.         local.u8_bits[j] = tmp;
41.     }
42. 
43.     return local;
44. }
45. //+------------------------------------------------------------------+
46. un_01 <ushort> Swap(const un_01 <ushort> &arg)
47. {
48.     un_01 <ushort> local;
49. 
50.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
51.     {
52.         tmp = arg.u8_bits[i];
53.         local.u8_bits[i] = arg.u8_bits[j];
54.         local.u8_bits[j] = tmp;
55.     }
56. 
57.     return local;
58. }
59. //+------------------------------------------------------------------+

代码 02

请注意,此代码 02 与代码 01 略有不同。尽管存在这些差异,但它仍然产生了相同的结果。也就是说,当你编译代码 02 并在 MetaTrader 5 终端中运行它时,你会得到如下所示的输出。

图 01

如您所见,即使将代码 01 的过程替换为片段 01,结果也与运行代码 01 得到的结果完全相同。但是,片段 01 限制了编译器在使用联合 un_01 时可以使用的数据类型。

此代码 02 也存在同样的限制。现在,我希望您注意片段 01 是如何重写的,以便我们可以使用函数而不是过程。请注意代码 02 中的第 19 行和第 28 行,其中我们使用了一个名为 “Swap” 的特殊变量,它实际上是一个函数,但设计为用作只读变量。

很酷吧?就像片段 01 显示了代码 01 中的过程会做什么一样,我们也可以将代码 02 中的函数转换为模板。这样,代码在选择类型方面将具有与代码 01 相同的行为和灵活性。为此,我们只需要概括代码 02 中函数的公共部分。然后,编译器可以在需要时动态替换数据类型。你理解了这个概念之后,你就可以像下面这样将代码 02 改写成代码 03。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T>
05. union un_01
06. {
07.     T value;
08.     uchar u8_bits[sizeof(T)];
09. };
10. //+------------------------------------------------------------------+
11. void OnStart(void)
12. {
13.     {
14.         un_01 <ulong> info;
15. 
16.         info.value = 0xA1B2C3D4E5F6789A;
17.         PrintFormat("The region is composed of %d bytes", sizeof(info));
18.         PrintFormat("Before modification: 0x%I64X", info.value);
19.         PrintFormat("After modification : 0x%I64X", Swap(info).value);
20.     }
21.     
22.     {
23.         un_01 <ushort> info;
24. 
25.         info.value = 0xCADA;
26.         PrintFormat("The region is composed of %d bytes", sizeof(info));
27.         PrintFormat("Before modification: 0x%I64X", info.value);
28.         PrintFormat("After modification : 0x%I64X", Swap(info).value);
29.     }
30. }
31. //+------------------------------------------------------------------+
32. template <typename T>
33. un_01 <T> Swap(const un_01 <T> &arg)
34. {
35.     un_01 <T> local;
36. 
37.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
38.     {
39.         tmp = arg.u8_bits[i];
40.         local.u8_bits[i] = arg.u8_bits[j];
41.         local.u8_bits[j] = tmp;
42.     }
43. 
44.     return local;
45. }
46. //+------------------------------------------------------------------+

代码 03

代码 03 代表了对代码 02 的改进,因为我们现在可以使用任何我们想要的数据类型。编译器能够正确地解释这一点,并自动生成我们的应用程序成功运行所需的所有重载。

有了这个,曾经看起来极其复杂和难以理解的事情变得简单明了。现在,即使是初学者也能轻松使用模板。你有没有注意到,那些乍看之下很复杂的事情,一旦我们真正理解了其中的基本概念,往往就会变得很简单?这就是为什么我一直坚持让你练习所展示的内容。不是为了记住写代码的方法,而是为了理解这些概念是如何在每个特定的上下文中应用的。

现在,这是许多人会归类为中级甚至高级的部分。然而,在我看来,到目前为止,我们所涵盖的仍然只是基础知识。我们还需要讨论一些我们一直在使用但尚未明确提及的重要内容:保留字 “typename”。

为了更好地解释这一点,我们需要另起一个类,以便我们能够冷静清晰地学习它。我们现在就开始吧。


Typename:它真正的用途是什么?

一个非常合理且重要的问题是:typename 究竟是什么?它在实际项目中的真正目的是什么?好吧,亲爱的读者,理解这一点可以帮助你实现一些非常有趣的代码类型。然而,一般来说,typename 用于非常具体的目的,通常与测试或类型检验有关。

至少据我所知,typename 很少用于确保编译器重载的函数或过程不会出现不可预测的行为。当我们将某物实现为模板时,在使用过程中遇到不一致或不连贯的结果并不罕见,这通常是因为模板没有正确处理某种类型。

在其他情况下,我们可能希望编译器重载的函数或过程根据所使用的数据类型表现不同。同一函数可以对一种类型以一种方式执行,对另一种类型则以另一种方式操作。这听起来可能令人困惑,但在实践中,有时可能是必要的。了解 typename 的工作原理将有助于您自信地应对此类情况。

为了证明这一点,让我们试着创建一个至少有点有趣的例子。因为使用 typename 可能是一个相当枯燥和技术性很强的主题。我们将努力使其更具吸引力。让我们使用下面的代码。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T>
05. union un_01
06. {
07.     T value;
08.     uchar u8_bits[sizeof(T)];
09. };
10. //+------------------------------------------------------------------+
11. void OnStart(void)
12. {
13.     Print("Demonstrating a mirror");
14. 
15.     Check((ulong)0xA1B2C3D4E5F6789A);
16.     Check((uint)0xFACADAFE);
17.     Check((ushort)0xCADE);
18.     Check((uchar)0xFE);
19. }
20. //+------------------------------------------------------------------+
21. template <typename T>
22. void Check(const T arg)
23. {
24.     un_01 <T> local;
25.     string sz;
26. 
27.     local.value = arg;
28.     PrintFormat("The region is composed of %d bytes", sizeof(local));
29.     PrintFormat("Before modification: 0x%I64X", local.value);
30.     PrintFormat("After modification : 0x%I64X", Mirror(local).value);
31.     StringInit(sz, 20, '*');
32.     Print(sz);
33. }
34. //+------------------------------------------------------------------+
35. template <typename T>
36. un_01 <T> Mirror(const un_01 <T> &arg)
37. {
38.     un_01 <T> local;
39. 
40.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
41.     {
42.         tmp = arg.u8_bits[i];
43.         local.u8_bits[i] = arg.u8_bits[j];
44.         local.u8_bits[j] = tmp;
45.     }
46. 
47.     return local;
48. }
49. //+------------------------------------------------------------------+

代码 04

在本代码 04 中,我们将对目前为止讨论过的所有内容进行一些尝试。我们的目标是了解如何在实际程序中使用 typename。目标是镜像(或“反映”)存储在内存(或在本例中,存储在变量中)的信息,以便右半部分与左半部分交换位置,非常简单。

当你查看代码 04 时,你会发现它本质上是我们一直在使用的示例的一个轻微变体。这是有意的,因为它使我们能够专注于新的和重要的理解。

当在 MetaTrader 5 中执行此代码 04 时,我们将得到如下所示的结果。

图 02

请注意,其中一个值已被突出显示。原因很简单:它不是镜像的。右侧并没有与左侧互换。但是,对于所有其他值,镜像都能完美工作。出现这个问题是因为当我们使用只占用一个字节的数据类型时,我们失去了将一侧镜像到另一侧的能力。

在 MQL5 中,我们知道只有两种类型符合这个单字节标准:uchar(用于无符号值)和 char(用于有符号值)。然而,实现单独的重载调用来单独处理这些类型会有点不方便,因为它会破坏我们操作的一致性。理想的解决方案是继续使用第 36 行实现的 Mirror 函数来处理这种行为。

但现在关键问题来了:我们如何告诉编译器如何处理 uchar 或 char 类型以执行镜像操作?

亲爱的读者,有很多方法可以做到这一点。一种实用且直接的方法是逐个读取这些位并交换它们 —— 右位换左位,左位换右位。这样就无需对函数或过程进行重载处理。然而,那并非我们的目标。将其视为一项家庭作业挑战:尝试实现一个逐位镜像数据的解决方案,交换输入信息的位。这是一个很好的练习,可以巩固这些概念,并开始像真正的程序员一样思考。

但就我们现在的情况而言:我们如何解决这个问题?好吧,这就是事情变得非常有趣的地方。你看,typename 可以直接告诉我们接收到的数据类型的名称。换句话说,我们可以问 typename 给定变量或参数的类型是什么。为了使这个概念更具体、更容易理解,让我们对代码 04 进行一个小修改。您可以在下面的代码中看到更新的版本。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T>
05. union un_01
06. {
07.     T value;
08.     uchar u8_bits[sizeof(T)];
09. };
10. //+------------------------------------------------------------------+
11. void OnStart(void)
12. {
13.     Print("Demonstrating a mirror");
14. 
15.     Check((ulong)0xA1B2C3D4E5F6789A);
16.     Check((uint)0xFACADAFE);
17.     Check((ushort)0xCADE);
18.     Check((uchar)0xFE);
19. }
20. //+------------------------------------------------------------------+
21. template <typename T>
22. void Check(const T arg)
23. {
24.     un_01 <T> local;
25.     string sz;
26. 
27.     local.value = arg;
28.     PrintFormat("The region is composed of %d bytes", sizeof(local));
29.     PrintFormat("Before modification: 0x%I64X", local.value);
30.     PrintFormat("After modification : 0x%I64X", Mirror(local).value);
31.     StringInit(sz, 20, '*');
32.     Print(sz);
33. }
34. //+------------------------------------------------------------------+
35. template <typename T>
36. un_01 <T> Mirror(const un_01 <T> &arg)
37. {
38.     un_01 <T> local;
39. 
40.     PrintFormat("Type is: [%s] or variable route is: {%s}", typename(T), typename(arg));
41.     for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
42.     {
43.         tmp = arg.u8_bits[i];
44.         local.u8_bits[i] = arg.u8_bits[j];
45.         local.u8_bits[j] = tmp;
46.     }
47.     
48.     return local;
49. }
50. //+------------------------------------------------------------------+

代码 05

当您运行此代码 05 时,您将看到与下图非常相似的内容。

图 03

现在,在图 03 中,出现了一些我完全无法理解的东西。这种样子完全不合理。所以它才用蓝色高亮显示。真是匪夷所思,但这不是关键。真正重要的是图中红色和绿色突出显示的区域。那么,这些信息是从哪里来的呢?这些输出是由于代码 05 中的第 40 行而打印出来的。现在请注意 —— 这一点非常重要,一定要理解清楚。否则,以后在使用图 03 中以红色和绿色突出显示的信息时,您可能会遇到问题。

所有红色突出显示部分都表示我们要求编译器告诉我们该函数使用的原始数据类型。另一方面,绿色突出显示部分对应于编译器对我们请求变量实际使用的数据类型的响应。这两个问题之间存在细微的差别,但答案可能完全不同。

通常情况下,你会看到很多程序员要求编译器显示变量的类型。然而,这并不总是给我们正确的答案,或者至少不是我们期望的答案。这是因为我们可能处于与上面所示类似的情况,其中原始数据类型是一回事,但变量数据类型更复杂,即使它以某种方式从原始类型派生出来。

但暂时不用担心如何检查这一点,下面附上了这些代码,供您学习和试验。回到我们的重点 —— 我们感兴趣的是图 03 中用红色突出显示的信息。请注意,所有这些都是按照代码实现阶段声明的方式编写的。但是,我想提请您注意一件非常重要的事情:这些值是字符串。这意味着我们可以在运行时将它们与其他字符串进行比较。而这才是关键所在。

现在我们有了工作的基础,我们只需要进行一个小测试,以隔离 uchar 或 char 类型(单字节类型),以便正确地反映值。为此,我们将进行一些小的改动。这次我们返回到原始代码 04 以进行必要的调整。您可以在下面看到修改后的版本。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T>
05. union un_01
06. {
07.     T value;
08.     uchar u8_bits[sizeof(T)];
09. };
10. //+------------------------------------------------------------------+
11. void OnStart(void)
12. {
13.     Print("Demonstrating a mirror");
14. 
15.     Check((ulong)0xA1B2C3D4E5F6789A);
16.     Check((uint)0xFACADAFE);
17.     Check((ushort)0xCADE);
18.     Check((uchar)0xFE);
19. }
20. //+------------------------------------------------------------------+
21. template <typename T>
22. void Check(const T arg)
23. {
24.     un_01 <T> local;
25.     string sz;
26. 
27.     local.value = arg;
28.     PrintFormat("The region is composed of %d bytes", sizeof(local));
29.     PrintFormat("Before modification: 0x%I64X", local.value);
30.     PrintFormat("After modification : 0x%I64X", Mirror(local).value);
31.     StringInit(sz, 20, '*');
32.     Print(sz);
33. }
34. //+------------------------------------------------------------------+
35. template <typename T>
36. un_01 <T> Mirror(const un_01 <T> &arg)
37. {
38.     un_01 <T> local;
39. 
40.     if (StringFind(typename(T), "char") > 0) local.value = (arg.value << 4) | (arg.value >> 4);
41.     else for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
42.     {
43.         tmp = arg.u8_bits[i];
44.         local.u8_bits[i] = arg.u8_bits[j];
45.         local.u8_bits[j] = tmp;
46.     }
47. 
48.     return local;
49. }
50. //+------------------------------------------------------------------+

代码 06

亲爱的读者,请密切注意。我在这里所做的事情可以通过许多不同的方式来实现。每种方法都有其优缺点,有些比其他方法更容易理解。所以,如果你没有完全理解代码 06 中发生的事情,不要担心。它在附件中提供,因此您可以对其进行修改和实验,直到一切变得有意义。

不过,在这样做之前,重要的是首先了解我实现了什么,以及运行此代码时的最终结果是什么。否则,你可能会改变一些东西,得到与预期不同的结果,错误地认为一切正常。

那么我们继续吧。首先,让我们看一下在 MetaTrader 5 终端中运行该程序的结果。你可以在下面看到。

图 04

简直太棒了,该代码成功实现了其目标,即镜像每个变量的值,使右半部分与左半部分交换,完全符合预期。但请注意,与代码 04 相比,代码 06 中唯一新增的内容是第 40 行(加上第 41 行的一个小调整)。不过,这不是我想强调的部分。我真正想让你注意的是我用来测试我们应该使用哪种方法进行镜像的函数。

MQL5 标准库中的 StringFind 函数允许我们在 typename 返回的字符串中搜索特定的子字符串。这一点非常重要,因为无论我们使用的是变量的声明类型还是编译器的原始类型,搜索到的片段都可能出现在其中任何一个类型中。以下是我特别使用此函数的关键原因:StringFind 可以成功检测到 “uchar” 和 “char”。两者唯一的区别在于 “uchar” 开头多了一个字母 “u”。因此,无论哪种方式,都将执行测试并返回结果。

也就是说,每种情况都是独一无二的。根据你在代码中要构建的内容,类型名称之间的这种微小差异,即使它们具有相同的字节大小,也会影响最终结果。这是因为在一种情况下,我们可能会有负值,而在另一种情况中,我们不会。当然,如果需要,我们可以显式地应用类型转换来纠正这一点。尽管如此,每当您依赖编译器生成的类型信息时,都值得注意。

现在,有趣的部分开始了。如您所见,图 04 中的结果是正确的,一切运行完美。但我们仍然需要考虑图 03 中看到的奇怪结果。我完全不知道编译器为什么要跟我们开个“小玩笑”。如果我们修改代码 06,使其显示与代码 05 打印的相同类型信息,一切就都正常了。然而,在编译代码 05 时发生了一件奇怪的事情,产生了图 03 中所示的奇怪输出。

为了证明这一点,修改后的代码如下所示。

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. template <typename T>
05. union un_01
06. {
07.     T value;
08.     uchar u8_bits[sizeof(T)];
09. };
10. //+------------------------------------------------------------------+
11. void OnStart(void)
12. {
13.     Print("Demonstrating a mirror");
14. 
15.     Check((ulong)0xA1B2C3D4E5F6789A);
16.     Check((uint)0xFACADAFE);
17.     Check((ushort)0xCADE);
18.     Check((uchar)0xFE);
19. }
20. //+------------------------------------------------------------------+
21. template <typename T>
22. void Check(const T arg)
23. {
24.     un_01 <T> local;
25.     string sz;
26. 
27.     local.value = arg;
28.     PrintFormat("The region is composed of %d bytes", sizeof(local));
29.     PrintFormat("Before modification: 0x%I64X", local.value);
30.     PrintFormat("After modification : 0x%I64X", Mirror(local).value);
31.     StringInit(sz, 20, '*');
32.     Print(sz);
33. }
34. //+------------------------------------------------------------------+
35. template <typename T>
36. un_01 <T> Mirror(const un_01 <T> &arg)
37. {
38.     un_01 <T> local;
39. 
40.     PrintFormat("Type is: [%s] or variable route is: {%s}", typename(T), typename(arg));
41.     if (StringFind(typename(T), "char") > 0) local.value = (arg.value << 4) | (arg.value >> 4);
42.     else for (uchar i = 0, j = sizeof(arg) - 1, tmp; i < j; i++, j--)
43.     {
44.         tmp = arg.u8_bits[i];
45.         local.u8_bits[i] = arg.u8_bits[j];
46.         local.u8_bits[j] = tmp;
47.     }
48. 
49.     return local;
50. }
51. //+------------------------------------------------------------------+

代码 07

请注意,代码 07 是代码 05 和代码 06 的组合。运行后,我们得到以下输出:

图 05

这难道不好笑吗?谁知道在代码 05 的编译或执行过程中到底发生了什么?值得庆幸的是,我们正在处理的例子中,一点怪癖或不一致并不重要。尽管如此,看到这些奇怪的编译器惊喜还是很有趣的。


最后的探讨

在本文中,我们总结了旨在帮助您真正理解函数和过程重载的解释和练习。目的是展示如何在不同的数据类型中重用相同的函数或过程名称。

我们从简单的例子开始这段旅程,例如在从基础到中级:重载中逐渐转向更复杂的案例,构建函数和过程的模板。由于这些概念不仅限于函数和过程,我们开始使用模板来减少编码工作。使用模板,我们有效地将重载版本的生成委托给编译器,使我们作为程序员的生活变得更加轻松。

然而,模板的概念可以进一步扩展,例如,创建和管理更复杂的数据类型。在本系列中,我们特别关注联合,将我们的示例保持在该建模框架内。这使我们能够尝试类型重载。这反过来又为用更少的代码实现更多功能打开了大门。编译器承担了保持一致性和正确性的责任。我们的任务很简单,就是告诉它使用哪种基本类型。

但是,我们还可以更进一步 —— 引入根据代码中使用的原始类型来控制行为的方法。为了实现这一点,我们使用 typename,它允许我们识别正在处理的确切类型,无论该类型是由编译器确定的还是由变量本身确定的。

我知道,对于你们中的许多人,尤其是初学者来说,所有这些可能看起来都很复杂或令人困惑。但请记住,我们仍然处于编程概念的最基本和最容易理解的水平。所以,亲爱的读者,我的建议是学习和实践我们在这里所涵盖的一切。请密切关注到目前为止这些文章中解释的每个概念,因为从现在开始,事情只会变得更具挑战性和趣味性。而对于那些真正热爱编程的人来说 —— 我们即将进入一个名为 MQL5 的全新领域。

在下一篇文章中见,我们将开始探索一个更有趣、更愉快的话题。

本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15671

附加的文件 |
Anexo.zip (3.8 KB)
创建动态多货币对EA(第二部分):投资组合多元化与优化 创建动态多货币对EA(第二部分):投资组合多元化与优化
投资组合多元化与优化旨在将投资有策略地分散配置于多种资产之上,在最小化风险的同时,依据风险调整后的绩效指标挑选出最理想的资产组合,从而实现回报最大化。
使用Python和MQL5进行特征工程(第四部分):基于UMAP回归的K线模式识别 使用Python和MQL5进行特征工程(第四部分):基于UMAP回归的K线模式识别
降维技术被广泛用于提升机器学习模型的性能。让我们来讨论一项被称为“统一流形逼近与投影”的相对较新的技术(UMAP)。这项新技术的开发旨在针对性地克服传统方法在数据中产生伪影和失真的局限性。UMAP是一种强大的降维技术,它能以一种新颖而有效的方式帮助我们将相似的K线进行分组,从而降低在样本外数据上的错误率,并提升我们的交易表现。
解密开盘区间突破(ORB)日内交易策略 解密开盘区间突破(ORB)日内交易策略
开盘区间突破(ORB)策略基于这样一种理念:市场开盘后不久确立的初始交易区间,反映了买卖双方就价格价值达成共识的重要水平。通过识别突破某一特定区间上方或下方的走势,交易者可以把握随之而来的市场契机——当市场方向愈发明朗时,这种契机往往会进一步显现。本文将探讨三种源自康克瑞图姆集团(Concretum Group)改良的ORB策略。
市场模拟(第六部分):将信息从 MetaTrader 5 传输到 Excel 市场模拟(第六部分):将信息从 MetaTrader 5 传输到 Excel
许多人,尤其是非程序员,发现在 MetaTrader 5 和其他程序之间传输信息非常困难。其中一个程序就是 Excel。许多人使用 Excel 作为管理和维护风险控制的一种方式。这是一个优秀的程序,易于学习,即使对于那些不是 VBA 程序员的人来说也是如此。在这里,我们将看看如何在 MetaTrader 5 和 Excel 之间建立连接(一种非常简单的方法)。