Как повторно использовать «генератор замыкания» в C++?

Я знал одну проблему: замыкания не ведут себя, как другие языки программирования (которые расширяют время жизни захваченных переменных и имеют 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++ способ, позволяющий нам повторно использовать «генератор», независимо от того, замыкание это или нет. Предпочтительна реализация без указателя. Спасибо.

Вы смотрите на лямбды с неправильной точки зрения. C++ — это не другие языки. Здесь нет «лексической среды», есть объекты, ссылки и управление памятью. Если вы хотите, чтобы две лямбды использовали один объект и при этом управление памятью было правильным, просто выполните первый фрагмент, но замените common на std::shared_ptr.

Passer By 04.08.2024 11:00

Ваши лямбды фиксируют переменные по значению. Это означает, что каждая лямбда получит копию значения переменной common. Затем функция generator заканчивается, и время жизни переменной common заканчивается, поэтому вы также не можете захватить ее по ссылке, если только не придумаете способ продлить время жизни common.

Some programmer dude 04.08.2024 11:04

Обратите внимание, что использование ссылки на локальную переменную, срок действия которой истек, является неопределенным поведением. «Неопределенное поведение означает, что может случиться что угодно, включая, помимо прочего, программу, выдающую ожидаемый результат. Но никогда не полагайтесь (и не делайте выводов на основе) на выходные данные программы, в которой есть UB. Программа может просто выйти из строя».

user12002570 04.08.2024 11:15

«способ в C++, позволяющий нам повторно использовать «генератор»…» Непонятно, что это значит. Каков ожидаемый результат для данного ввода.

user12002570 04.08.2024 11:17

Вам пришлось добавить [=] и mutable, чтобы получить независимые приращения в первой версии. Как вы можете при этом говорить, что «ожидаете, что [] получите 1 2»?

Davis Herring 04.08.2024 11:21

@user12002570,Спасибо за ответ, в последнем случае мне желательно получить 8 9 3 4. Я ожидаю, что pair1 и pair2 будут «разными замыканиями», то есть adder1 из pair1 и adder1 из pair2 будут увеличиваться отдельно, до своей собственной копии common. В результате получается 2 common и у каждого из них есть adder1 и adder2. Всего 4 закрытия

miyou379 04.08.2024 11:58

@PasserBy Спасибо! «две лямбды для совместного использования одного объекта» — это то, что я хочу

miyou379 04.08.2024 12:04

@miyou379 Вы можете использовать трюк decltype([](){}), как показано в моем ответе

user12002570 04.08.2024 12:26
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
8
86
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Мне нужно получить 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 05.08.2024 17:53

@melston По сути, он использует тот факт, что каждая лямбда имеет тип, отличный от любой другой лямбды. Это должно прояснить, что это делает. Я использую его регулярно, когда мне нужно создать отдельные типы. Я не думаю, что у трюка есть название. Может быть, мы сможем изобрести его, поскольку я использую его регулярно. Готовы к предложениям.

user12002570 05.08.2024 18:06

Вы хотите сказать, что лямбды имеют отдельный тип, хотя тип тот же (decltype тот же)?

melston 05.08.2024 22:50

@melston Да, именно. Вы можете проверить это с помощью static_assert или просто добавив статическую переменную, как в этой демонстрации. Каждый раз, когда вы вызываете функцию с аргументом по умолчанию в этой демонстрации, вы получаете разные типы!

user12002570 05.08.2024 22:59

Спасибо @user12002570. Это интересно. Я не понимаю, как это происходит, поскольку параметр типа тот же. Где я могу найти дополнительную информацию об этом?

melston 06.08.2024 15:22

@melston Чтобы упростить задачу: каждый раз, когда мы вызываем приведенный выше шаблон функции без каких-либо аргументов шаблона, будет использоваться аргумент по умолчанию — decltype[](){}. То есть, скажем, мы набираем f<>(), тогда это эквивалентно написанию f<decltype([](){})>, что, я уверен, вы уже знаете. Аналогично, если мы теперь снова вызовем f<>(), произойдет то же самое, decltype([](){}) будет использовано, что означает, что был передан другой отдельный тип. Один вопрос: Зачем создавать язык с уникальными анонимными типами?

user12002570 06.08.2024 15:57

Ах. Спасибо. Я не знал об ограничении уникальных анонимных типов.

melston 06.08.2024 21:50
Ответ принят как подходящий

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.

Другие вопросы по теме