воскресенье, 23 ноября 2014 г.

Коротко. Про лямбды и копирующие конструкторы в C++

http://programmingmindstream.blogspot.ru/2014/11/delphi_21.html?showComment=1416695404969#c6071212647984907418

Есть "поучительная история" про "лямбды".

Про C++.

https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BC%D1%8B%D0%BA%D0%B0%D0%BD%D0%B8%D0%B5_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)

https://ru.wikipedia.org/wiki/C%2B%2B11#.D0.9B.D1.8F.D0.BC.D0.B1.D0.B4.D0.B0-.D1.84.D1.83.D0.BD.D0.BA.D1.86.D0.B8.D0.B8_.D0.B8_.D0.B2.D1.8B.D1.80.D0.B0.D0.B6.D0.B5.D0.BD.D0.B8.D1.8F

Процитирую:

"

Лямбда-функции и выражения

В стандартном C++, например, при использовании алгоритмов стандартной библиотеки C++ sort и find, часто возникает потребность в определении функций-предикатов рядом с местом, где осуществляется вызов этого алгоритма. В языке существует только один механизм для этого: возможность определить класс функтора (передача экземпляра класса, определенного внутри функции, в алгоритмы запрещена (Meyers, Effective STL)). Зачастую данный способ является слишком избыточным и многословным и лишь затрудняет чтение кода. Кроме того, стандартные правила C++ для классов, определённых в функциях, не позволяют использовать их в шаблонах и таким образом делают их применение невозможным.
Очевидным решением проблемы явилось разрешение определения лямбда-выражений и лямбда-функций в C++11. Лямбда-функция определяется следующим образом:
[](int x, int y) { return x + y; }
Тип возвращаемого значения этой безымянной функции вычисляется как decltype(x+y). Тип возвращаемого значения может быть опущен только в том случае, если лямбда-функция представлена в форме return expression. Это ограничивает размер лямбда-функции до одного выражения.
Тип возвращаемого значения может быть указан явно, например:
[](int x, int y) -> int { int z = x + y; return z; }
В этом примере создаётся временная переменная z для хранения промежуточного значения. Как и в нормальных функциях, это промежуточное значение не сохраняется между вызовами.
Тип возвращаемого значения может быть полностью опущен, если функция не возвращает значения (то есть тип возвращаемого значения — void)
Также возможно использование ссылок на переменные, определённые в той же области видимости, что и лямбда-функция. Набор таких переменных обычно называют замыканием. Замыкания определяются и используются следующим образом:"

И ещё:

"std::vector<int> someList;
int total = 0;
std::for_each(someList.begin(), someList.end(), [&total](int x) {
  total += x;
});
std::cout << total;
Это отобразит сумму всех элементов в списке. Переменная total хранится как часть замыкания лямбда-функции. Так как она ссылается на стековую переменную total, она может менять её значение.
Переменные замыкания для локальных переменных могут быть также определены без использования символа ссылки &, что означает, что функция будет копировать значение. Это вынуждает пользователя заявлять о намерении сослаться на локальную переменную или скопировать её."

Много букв процитировал.

А сам напишу коротко:

Пусть есть класс:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

- обратим внимание - тут НЕТ нормального копирующего конструктора и оператора присваивания.

Теперь пусть есть код:

 A x ("Hello world");
 char * y;
 std::vector<int> someList = {1};
 // - просто чтобы вектор был не пуст, хотя это и не так важно
 std::for_each(someList.begin(), someList.end(), [](int anItem) {
   y := x.getString();
   // - типа просто получили ссылку на строку и ничего с ней не делаем
 });

- казалось бы - "ничего такого" - мы тут не сделали.

Просто один раз дёрнули подитеративную функцию.

Однако мы получим AV.

Почему?

А потому, что по-умолчанию лямбды "захватывают" все внешние переменные - по значению.

Я ведь - не зря всё это выше цитировал.

Что тут происходит?

Обратим внимание на строчку:

 y := x.getString();

- казалось бы тут "ничего такого".

Однако - это не так.

Если эту строчку закомментировать, то AV - пропадёт.

В чём же дело?

А вот в чём.

Мы ведь зовём x.getString() внутри анонимного метода.

И что происходит?

А то, что за x.getString() - скрывается copy_of_x.getString(), которая получается при создании анонимного метода.

А как получается?

По значению. Читаем выше.

И как по значению? Через вызов копирующего конструктора, который у нас - умолчательный.

Который просто копирует поля класса.

Соответственно поле m_String просто копируется при получении copy_of_x из x.

А освобождается эта строка ДВА раза - для x и для copy_of_x.

Откуда AV - теперь понятно?

Теперь что же делать?

Самый простой вариант это:

 A x ("Hello world");
 char * y;
 std::vector<int> someList = {1};
 // - просто чтобы вектор был не пуст, хотя это и не так важно
 std::for_each(someList.begin(), someList.end(), [&x](int anItem) {
   y := x.getString();
   // - типа просто получили ссылку на строку и ничего с ней не делаем
 });

- что мы тут сделали?

Мы написали - [&x] вместо [].

Этим мы сказали, что x явно передаётся по ссылке, а не по значению.

И тогда - копия сниматься - не будет.

Ну или можно определить копирующий конструктор:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

 A (const A & anOther)
 {
  m_String = strnew(anOther.m_String);
  // - получили копию строки
 }

 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

-- тогда при снятии копии будет вызываться "правильный" копирующий конструктор, который будет снимать копию строки, а не просто копировать указатель.

И тогда будут работать оба варианта вызова - и [] и [&x].

Теперь как сделать так, чтобы вызов по значению с [] был невозможен?

Ну пусть природа нашего класса такова, что его нельзя копировать.

Как это сделать?

Ну в "обычном C++" можно объявить приватный пустой копирующий конструктор:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

private:
 A (const A & anOther);

public:
 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

- и тогда на "факт копирования" ругнётся компилятор. Скажет "неопределённый копирующий конструктор".

В C++11 можно сделать более "красиво" - удалить копирующий конструктор по-умолчанию:

class A
{
private:
 char * m_String;
public
 A (char * aString)
 {
  m_String = strnew(aString);
  // - получили копию строки
 }

 A (const A & anOther) = delete;

 ~A()
 {
  strdispose(m_String);
  // - освободили строку
 }

 const char * getString()
 {
  return m_String;
 }
};

- результат будет тем же. На "факт копирования" ругнётся компилятор.

Ну вот собственно пока и всё.

Ни на что не претендую.

Нет. Ещё не всё...

Про Objective-C - забыл.

Там можно написать так:

 A x ("Hello world");
 char * y;
 [m_HiddenTextIndex enumerateKeysAndObjectsUsingBlock:^(id docId, id data, BOOL *) {
  y := x.getString();
  // - типа просто получили ссылку на строку и ничего с ней не делаем
 }];

-- ^(id docId, id data, BOOL *) - это тоже анонимная функция.

И внутри неё используется x.getString(), которая тоже copy_of_x.getString().

И этой "лямбде" нет средств указать, что передаём по значению.

Ну кроме модификатора __block. Но там тоже - "есть нюансы".

И посему - для такого кода можно только или запретить копирующий конструктор, или определить его правильно.

Вот - теперь всё.

Комментариев нет:

Отправить комментарий