Указатели, ссылки и const

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

Ссылки в MQL5 используются только при описании параметров функций и методов. Более того, параметры объектных типов обязательно должны передаваться по ссылке.

void function(ClassOrStruct &object) { }          // можно
void function(ClassOrStruct object) { }           // нельзя
void function(double &value) { }                  // можно
void function(double value) { }                   // можно

Здесь ClassOrStruct — имя класса или структуры.

В качестве аргумента для параметра типа ссылки допустимо передавать только переменные (LValue), но не константы или временные значения, полученные в результате вычисления выражения.

Создать переменную ссылочного типа или вернуть ссылку из функции нельзя.

ClassOrStruct &function(void) { return Class(); } // нельзя
ClassOrStruct &object;                            // нельзя
double &value;                                    // нельзя

Указатели в MQL5 доступны только для объектов классов. Указатели на переменные встроенных типов или структур не поддерживаются.

Вы можете объявить переменную или параметр функции типа указатель на объект, а также вернуть указатель на объект из функции.

ClassOrStruct *pointer;                                   // можно
void function(ClassOrStruct *object) { }                  // можно
ClassOrStruct *function() { return new ClassOrStruct(); } // можно

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

Если функция вернула указатель на объект, распределенный динамически внутри функции с помощью new, то вызывающий код должен "не забыть" освободить указатель с помощью delete.

Указатель, в отличие от ссылки, может быть равен NULL. Параметры-указатели могут иметь значение по умолчанию, ссылки — нет (ошибка "reference cannot be initialized").

void function(ClassOrStruct *object = NULL) { }          // можно
void function(ClassOrStruct &object = NULL) { }          // нельзя

Ссылки и указатели могут быть совмещены в описании параметра. Так функция может принимать ссылку на указатель: тогда изменения указателя в функции станут доступны в вызывающем коде. В частности, подобным образом можно реализовать фабричную функцию, которая ответственна за создание объектов.

void createObject(ClassName *&ref)
{
   ref = new ClassName();
   // дальнейшая настройка ref
   ...
}

Правда, для возврата из функции одного указателя обычно принято использовать оператор return, так что данный пример — несколько искусственный. Однако в тех случаях, когда передать наружу необходимо массив указателей, ссылка на него в параметре становится предпочтительным вариантом. Например, в некоторых классах стандартной библиотеки для работы с классами-контейнерами типа карт с парами [ключ,значение] (MQL5/Include/Generic/SortedMap.mqh, MQL5/Include/Generic/HashMap.mqh) имеются методы CopyTo для получения массивов с элементами CKeyValuePair.

int CopyTo(CKeyValuePair<TKey,TValue> *&dst_array[], const int dst_start 0);

Тип параметра dst_array может показаться незнакомым: это шаблон класса. Мы изучим шаблоны в следующей главе. Здесь для нас пока важно лишь то, что это ссылка на массив указателей.

Особое поведение для всех типов накладывает модификатор const. В отношении встроенных типов он был рассмотрен в разделе Переменные-константы. Для объектных типов есть свои особенности.

Если переменная или параметр функции описан как указатель или ссылка на объект (ссылка — только в случае параметра), то наличие у них модификатора const ограничивает набор методов и свойств, к которым можно обращаться, только теми, что также имеют модификатор const. Иными словами, через константные ссылки и указатели доступны только константные свойства.

При попытке вызвать неконстантный метод или изменить неконстантное поле компилятор выдаст ошибку: "вызов неконстантного метода для константного объекта" ("call non-const method for constant object") или "константу нельзя изменить" ("constant cannot be modified").

Неконстантный параметр-указатель может принять любой аргумент (как константу, так и неконстанту).

Следует иметь в виду, что в описании указателя можно ставить два модификатора const: один будет относиться к объекту, а второй — к указателю:

  • Class *pointer — указатель на объект; объект и указатель работают без ограничений;
  • const Class *pointer — указатель на константный объект; для объекта доступны только константные методы и чтение свойств, но указатель можно изменить (присвоить ему адрес другого объекта);
  • const Class * const pointer — константный указатель на константный объект; для объекта доступны только константные методы и чтение свойств; указатель менять нельзя;
  • Class * const pointer — константный указатель на объект; указатель менять нельзя, а свойства объекта — можно.

Рассмотрим в качестве примера следующий класс Counter (CounterConstPtr.mq5).

class Counter
{
public:
   int counter;
   
   Counter(const int n = 0) : counter(n) { }
   
   void increment()
   {
      ++counter;
   }
   
   Counter *clone() const
   {
      return new Counter(counter);
   }
};

В нем искусственно сделана публичной переменная counter. Также в классе имеется два метода, один из которых константный (clone), а второй — нет (increment). Напомним, что константный метод не имеет права менять поля объекта.

Следующая функция с параметром типа Counter *ptr может вызывать все методы класса и менять его поля.

void functionVolatile(Counter *ptr)
{
   // OK: все доступно
   ptr.increment();
   ptr.counter += 2;
   // удаляем клон сразу, чтобы не было утечки памяти
   // клон нужен только для демонстрации вызова константного метода 
   delete ptr.clone(); 
   ptr = NULL;
}

Следующая функция с параметром const Counter *ptr вызовет пару ошибок.

void functionConst(const Counter *ptr)
{
   // ОШИБКИ:
   ptr.increment(); // call non-const method for constant object
   ptr.counter = 1// constant cannot be modified
   
   // OK: доступны только const-методы, поля можно читать
   Print(ptr.counter); // чтение const-объекта
   Counter *clone = ptr.clone(); // вызов const-метода
   ptr = clone;     // изменяем non-const указатель ptr
   delete ptr;      // чистим память
}

Наконец, следующая функция с параметром const Counter * const ptr позволяет еще меньше.

void functionConstConst(const Counter * const ptr)
{
   // OK: доступны только const-методы, ptr нельзя менять
   Print(ptr.counter); // чтение const-объекта
   delete ptr.clone(); // вызов const-метода
   
   Counter local(0);
   // ОШИБКИ:
   ptr.increment(); // call non-const method for constant object
   ptr.counter = 1// constant cannot be modified
   ptr = &local;    // constant cannot be modified
}

В функции OnStart, где мы описали два объекта Counter (один константный, а другой нет), можно вызывать эти функции с некоторыми исключениями:

void OnStart()
{
   Counter counter;
   const Counter constCounter;
   
   counter.increment();
   
   // ОШИБКА:
   // constCounter.increment(); // call non-const method for constant object
   Counter *ptr = (Counter *)&constCounter// трюк: приведение типа без const
   ptr.increment();
   
   functionVolatile(&counter);
   
   // ОШИБКА: cannot convert from const pointer...
   // functionVolatile(&constCounter); // to nonconst pointer
   
   functionVolatile((Counter *)&constCounter); // приведение типа без const
   
   functionConst(&counter);
   functionConst(&constCounter);
   
   functionConstConst(&counter);
   functionConstConst(&constCounter);
}

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

Во-вторых, в функцию functionVolatile нельзя передать constCounter — получаем ошибку "нельзя конвертировать константный указатель в неконстантный" ("cannot convert from const pointer to nonconst pointer").

Однако обе ошибки можно обойти с помощью явного приведения типа без модификатора const. Хотя так делать не рекомендуется.