Я знал одну проблему: замыкания не ведут себя, как другие языки программирования (которые расширяют время жизни захваченных переменных и имеют GC) в C++. если мы напишем такой код
auto generator() {
int common = 0;
auto adder1 = [=]() mutable {
++common;
return common;
};
auto adder2 = [=]() mutable {
++common;
return common;
};
return make_pair(adder1, adder2);
}
int main() {
auto pair = generator();
auto a1 = pair.first;
auto a2 = pair.second;
cout << a1() << " " << a2() << "\n";
return 0;
}
ожидается, что мы получим 1 2
, поэтому common
является «лексической средой» как для adder1
, так и для adder2
. Однако мы получим 1 1
, потому что common
копируется по значению, а не по ссылке.
Решение этой проблемы — позволить common
быть static
. Но, на мой взгляд, static
действительно продлевает срок службы common
так, как нам этого не хочется. (Может быть, у меня какое-то недопонимание). Поэтому я бы предпочел использовать другое решение, использующее другое замыкание и инициализированные захваты лямбда-выражений (в C++14) для генерации adder
, захватывая common
по ссылке.
auto generator = [common = 0]() mutable {
auto adder1 = [&]() {
++common;
return common;
};
auto adder2 = [&]() {
++common;
return common;
};
return make_pair(adder1, adder2);
};
на этот раз все работает хорошо и выдает 1 2
.
Мы используем функцию для инициализации adder
, как generator
выше. В общем, это не «закрытие» или, по крайней мере, не то закрытие, которое специально хотелось. Вот почему я считаю их «генераторами замыканий».
Даже во втором издании это, казалось бы, работало хорошо, генератор не создает разные common
для разных (adder1, adder2)
пар. Если мы вызовем генератор следующим образом:
int main() {
auto pair1 = generator(); auto pair2 = generator();
auto a1 = pair1.first; auto a2 = pair1.second;
auto a3 = pair2.first; auto a4 = pair2.second;
a1(); a1(); a1(); a2(); a2(); a2(); a2(); a3(); a4();
cout << a1() << " " << a2() << " " << a3() << " " << a4() << "\n";
return 0;
}
он выведет 10 11 12 13
, но не 8 9 3 4
. Поскольку генератор сам по себе является замыканием, следовательно, common
, присвоенный pair1
и pair2
, один и тот же.
Поэтому мне интересно, существует ли в C++ способ, позволяющий нам повторно использовать «генератор», независимо от того, замыкание это или нет. Предпочтительна реализация без указателя. Спасибо.
Ваши лямбды фиксируют переменные по значению. Это означает, что каждая лямбда получит копию значения переменной common
. Затем функция generator
заканчивается, и время жизни переменной common
заканчивается, поэтому вы также не можете захватить ее по ссылке, если только не придумаете способ продлить время жизни common
.
Обратите внимание, что использование ссылки на локальную переменную, срок действия которой истек, является неопределенным поведением. «Неопределенное поведение означает, что может случиться что угодно, включая, помимо прочего, программу, выдающую ожидаемый результат. Но никогда не полагайтесь (и не делайте выводов на основе) на выходные данные программы, в которой есть UB. Программа может просто выйти из строя».
«способ в C++, позволяющий нам повторно использовать «генератор»…» Непонятно, что это значит. Каков ожидаемый результат для данного ввода.
Вам пришлось добавить [=]
и mutable
, чтобы получить независимые приращения в первой версии. Как вы можете при этом говорить, что «ожидаете, что [] получите 1 2
»?
@user12002570,Спасибо за ответ, в последнем случае мне желательно получить 8 9 3 4
. Я ожидаю, что pair1
и pair2
будут «разными замыканиями», то есть adder1
из pair1
и adder1
из pair2
будут увеличиваться отдельно, до своей собственной копии common
. В результате получается 2 common
и у каждого из них есть adder1
и adder2
. Всего 4 закрытия
@PasserBy Спасибо! «две лямбды для совместного использования одного объекта» — это то, что я хочу
@miyou379 Вы можете использовать трюк decltype([](){})
, как показано в моем ответе
Мне нужно получить 8 9 3 4 . «две лямбды для совместного использования одного объекта» — это то, что я хочу
Вы можете использовать трюк decltype([](){})
, создав generated
шаблонную сущность и используя ее аргумент по умолчанию, как показано ниже:
//made this a template
template<typename T>
auto generator = [common = 0]() mutable {
//same code here as before
return make_pair(adder1, adder2);
};
//helper function template
template<typename T = decltype([](){})>
auto generator_t()
{
return generator<T>();
}
int main() {
auto pair1 = generator_t(); auto pair2 = generator_t();
//same code here as before
cout << a1() << " " << a2() << " " << a3() << " " << a4() << "\n"; //prints 8 9 3 4
return 0;
}
Я не знаком с этим «трюком» и не понимаю, что он делает. Есть ссылка на объяснение?
@melston По сути, он использует тот факт, что каждая лямбда имеет тип, отличный от любой другой лямбды. Это должно прояснить, что это делает. Я использую его регулярно, когда мне нужно создать отдельные типы. Я не думаю, что у трюка есть название. Может быть, мы сможем изобрести его, поскольку я использую его регулярно. Готовы к предложениям.
Вы хотите сказать, что лямбды имеют отдельный тип, хотя тип тот же (decltype тот же)?
@melston Да, именно. Вы можете проверить это с помощью static_assert или просто добавив статическую переменную, как в этой демонстрации. Каждый раз, когда вы вызываете функцию с аргументом по умолчанию в этой демонстрации, вы получаете разные типы!
Спасибо @user12002570. Это интересно. Я не понимаю, как это происходит, поскольку параметр типа тот же. Где я могу найти дополнительную информацию об этом?
@melston Чтобы упростить задачу: каждый раз, когда мы вызываем приведенный выше шаблон функции без каких-либо аргументов шаблона, будет использоваться аргумент по умолчанию — decltype[](){}
. То есть, скажем, мы набираем f<>()
, тогда это эквивалентно написанию f<decltype([](){})>
, что, я уверен, вы уже знаете. Аналогично, если мы теперь снова вызовем f<>()
, произойдет то же самое, decltype([](){})
будет использовано, что означает, что был передан другой отдельный тип. Один вопрос: Зачем создавать язык с уникальными анонимными типами?
Ах. Спасибо. Я не знал об ограничении уникальных анонимных типов.
generator
хранит состояние common
и должен быть объектом, чтобы C++ мог управлять временем жизни этого состояния. Рассмотрим просто:
#include <functional>
#include <iostream>
class generator {
int common = 0;
int get() { return ++common; }
public:
std::function<int()> first = std::bind(&generator::get, this);
std::function<int()> second = std::bind(&generator::get, this);
};
int main() {
auto pair = generator(); // lifetime of common begins
auto a1 = pair.first;
auto a2 = pair.second;
std::cout << a1() << " " << a2() << "\n";
return 0; // lifetime of common ends
}
Вы также можете управлять временем жизни по-другому. Как вы заметили, можно заработать всю жизнь static
. Вы также можете использовать динамическое размещение или использовать std::shared_ptr
, чтобы продлить срок службы. Я думаю, что время жизни common
должно быть привязано к времени жизни пары - таким образом, это объект, который содержит как пару функций, так и common
.
Вы смотрите на лямбды с неправильной точки зрения. C++ — это не другие языки. Здесь нет «лексической среды», есть объекты, ссылки и управление памятью. Если вы хотите, чтобы две лямбды использовали один объект и при этом управление памятью было правильным, просто выполните первый фрагмент, но замените
common
наstd::shared_ptr
.