В синтаксисе лямбда С++ 11 выделены кучи?
С++ 11 lambdas великолепны!
Но не хватает одной вещи, которая заключается в том, как безопасно обрабатывать изменяемые данные.
Ниже перечислены плохие счета после первого счета:
#include <cstdio>
#include <functional>
#include <memory>
std::function<int(void)> f1()
{
int k = 121;
return std::function<int(void)>([&]{return k++;});
}
int main()
{
int j = 50;
auto g = f1();
printf("%d\n", g());
printf("%d\n", g());
printf("%d\n", g());
printf("%d\n", g());
}
дает
$ g++-4.5 -std=c++0x -o test test.cpp && ./test
121
8365280
8365280
8365280
Причина в том, что после возврата f1()
k
выходит за пределы области видимости, но все еще находится в стеке. Итак, в первый раз g()
выполняется k
отлично, но после этого стек поврежден и k
теряет свое значение.
Таким образом, единственный способ, которым я смог сделать безопасное возвратное закрытие в С++ 11, - явно выделить закрытые переменные в куче:
std::function<int(void)> f2()
{
int k = 121;
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
return std::function<int(void)>([=]{return (*o)++;});
}
int main()
{
int j = 50;
auto g = f2();
printf("%d\n", g());
printf("%d\n", g());
printf("%d\n", g());
printf("%d\n", g());
}
Здесь [=]
используется для обеспечения копирования общего указателя, а не ссылки, чтобы обработка памяти выполнялась правильно: выделенная кучей копия k
должна быть освобождена при выходе из сгенерированной функции g
объема. Результат по желанию,
$ g++-4.5 -std=c++0x -o test test.cpp && ./test
121
122
123
124
Это довольно уродливо ссылаться на переменные путем их разыменования, но вместо этого должно быть возможно использовать ссылки:
std::function<int(void)> f3()
{
int k = 121;
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
int &p = *o;
return std::function<int(void)>([&]{return p++;});
}
Собственно, это странно дает мне,
$ g++-4.5 -std=c++0x -o test test.cpp && ./test
0
1
2
3
Любая идея, почему? Может быть, не вежливо взять ссылку на общий указатель, теперь, когда я думаю об этом, так как это не отслеживаемая ссылка. Я обнаружил, что перемещение ссылки внутрь лямбда вызывает сбой,
std::function<int(void)> f4()
{
int k = 121;
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
return std::function<int(void)>([&]{int &p = *o; return p++;});
}
даяние,
g++-4.5 -std=c++0x -o test test.cpp && ./test
156565552
/bin/bash: line 1: 25219 Segmentation fault ./test
В любом случае было бы неплохо, если бы был способ автоматически сделать безопасное возвратное закрытие посредством распределения кучи. Например, если была альтернатива [=]
и [&]
, которая указывала, что переменные должны быть выделены в кучу и ссылаться через ссылки на общие указатели. Моя первоначальная мысль, когда я узнал о std::function
, заключалась в том, что она создает объект, инкапсулирующий замыкание, поэтому он может обеспечить хранение для среды закрытия, но мои эксперименты показывают, что это, похоже, не помогает.
Я думаю, что безопасно возвратные закрытия в С++ 11 будут иметь первостепенное значение для их использования, кто-нибудь знает, как это можно сделать более элегантно?
Ответы
Ответ 1
В f1
вы получаете поведение undefined по причине, которую вы говорите; lambda содержит ссылку на локальную переменную, а после возврата функции ссылка больше недействительна. Чтобы обойти это, вам не нужно выделять кучу, вам просто нужно объявить, что зафиксированные значения изменяемы:
int k = 121;
return std::function<int(void)>([=]() mutable {return k++;});
Вы должны быть осторожны в использовании этой лямбда, потому что разные копии ее будут изменять свою собственную копию захваченной переменной. Часто алгоритмы предполагают, что использование копии функтора эквивалентно использованию оригинала. Я думаю, что существует только один алгоритм, который фактически делает допуски для объекта с функцией состояния, std:: for_each, где он возвращает другую копию объекта функции, который он использует, чтобы вы могли получить доступ к тем изменениям.
В f3
ничего не поддерживается копией общего указателя, поэтому память освобождается и доступ к ней дает поведение undefined. Вы можете исправить это, явно захватив общий указатель по значению и по-прежнему захватить указатель на int по ссылке.
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
int &p = *o;
return std::function<int(void)>([&p,o]{return p++;});
f4
снова работает undefined, потому что вы снова захватываете ссылку на локальную переменную o
. Вы должны просто захватить по значению, но затем создать свой int &p
внутри лямбды, чтобы получить синтаксис, который вы хотите.
std::shared_ptr<int> o = std::shared_ptr<int>(new int(k));
return std::function<int(void)>([o]() -> int {int &p = *o; return p++;});
Обратите внимание, что при добавлении второго оператора С++ 11 больше не нужно опускать возвращаемый тип. (clang и я предполагаю, что gcc имеет расширение, которое позволяет выводить тип вывода даже с несколькими операторами, но вы должны получить предупреждение как минимум.)
Ответ 2
Вот мой тестовый код. Он использует рекурсивную функцию для сравнения адреса лямбда-параметров с другими переменными на основе стека.
#include <stdio.h>
#include <functional>
void fun2( std::function<void()> callback ) {
(callback)();
}
void fun1(int n) {
if(n <= 0) return;
printf("stack address = %p, ", &n);
fun2([n]() {
printf("capture address = %p\n", &n);
fun1(n - 1);
});
}
int main() {
fun1(200);
return 0;
}
Скомпилируйте код с помощью mingw64 и запустите на Win7, выведите
stack address = 000000000022F1E0, capture address = 00000000002F6D20
stack address = 000000000022F0C0, capture address = 00000000002F6D40
stack address = 000000000022EFA0, capture address = 00000000002F6D60
stack address = 000000000022EE80, capture address = 00000000002F6D80
stack address = 000000000022ED60, capture address = 00000000002F6DA0
stack address = 000000000022EC40, capture address = 00000000002F6DC0
stack address = 000000000022EB20, capture address = 00000000007A7810
stack address = 000000000022EA00, capture address = 00000000007A7820
stack address = 000000000022E8E0, capture address = 00000000007A7830
stack address = 000000000022E7C0, capture address = 00000000007A7840
Очевидно, что захваченный параметр не расположен в области стека,
и адрес захваченных параметров не непрерывный.
Итак, я считаю, что некоторые компиляторы могут использовать динамическое распределение памяти для
захватить параметры лямбда.