20.7 — Przechwytywanie lambda


Klauzule przechwytywania i przechwytywanie według wartości

W poprzedniej lekcji (20.6 -- Wprowadzenie do lambd (funkcji anonimowych)), przedstawiliśmy ten przykład:

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>

int main()
{
  std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };

  auto found{ std::find_if(arr.begin(), arr.end(),
                           [](std::string_view str)
                           {
                             return str.find("nut") != std::string_view::npos;
                           }) };

  if (found == arr.end())
  {
    std::cout << "No nuts\n";
  }
  else
  {
    std::cout << "Found " << *found << '\n';
  }

  return 0;
}

Teraz zmodyfikujmy przykład nakrętki i pozwól użytkownikowi wybrać podciąg do wyszukania. To nie jest tak intuicyjne, jak można by się spodziewać.

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>

int main()
{
  std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };

  // Ask the user what to search for.
  std::cout << "search for: ";

  std::string search{};
  std::cin >> search;

  auto found{ std::find_if(arr.begin(), arr.end(), [](std::string_view str) {
    // Search for @search rather than "nut".
    return str.find(search) != std::string_view::npos; // Error: search not accessible in this scope
  }) };

  if (found == arr.end())
  {
    std::cout << "Not found\n";
  }
  else
  {
    std::cout << "Found " << *found << '\n';
  }

  return 0;
}

Ten kod nie zostanie skompilowany. W przeciwieństwie do bloków zagnieżdżonych, gdzie dowolny identyfikator dostępny w bloku zewnętrznym jest dostępny w bloku zagnieżdżonym, wyrażenia lambda mogą uzyskać dostęp tylko do niektórych rodzajów obiektów, które zostały zdefiniowane poza lambdą. Obejmuje to:

  • Obiekty o statycznym (lub lokalnym wątku) czasie przechowywania (w tym zmienne globalne i statyczne lokalne)
  • Obiekty, które są constexpr (jawnie lub pośrednio)

Ponieważ search nie spełniają żadnego z tych wymagań, lambda ich nie widzi.

Wskazówka

Lambda może uzyskać dostęp tylko do niektórych rodzajów obiektów, które zostały zdefiniowane poza lambdą, w tym do tych z czas trwania przechowywania statycznego (np. zmienne globalne i statyczne lokalne) oraz obiekty constexpr.

Aby uzyskać dostęp search z poziomu lambdy, będziemy musieli użyć klauzuli przechwytywania.

Klauzula przechwytywania

Klasa klauzula przechwytywania jest używana do (pośredniego) zapewnienia lambdzie dostępu do zmiennych dostępnych w otaczającym zakresie, tak jak zwykle nie mieć dostępu do. Wszystko, co musimy zrobić, to wyświetlić listę jednostek, do których chcemy uzyskać dostęp, z poziomu lambda w ramach klauzuli przechwytywania. W tym przypadku chcemy dać naszej lambdzie dostęp do wartości zmiennej search, więc dodajemy ją do klauzuli przechwytywania:

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>
#include <string>

int main()
{
  std::array<std::string_view, 4> arr{ "apple", "banana", "walnut", "lemon" };

  std::cout << "search for: ";

  std::string search{};
  std::cin >> search;

  // Capture @search                                vvvvvv
  auto found{ std::find_if(arr.begin(), arr.end(), [search](std::string_view str) {
    return str.find(search) != std::string_view::npos;
  }) };

  if (found == arr.end())
  {
    std::cout << "Not found\n";
  }
  else
  {
    std::cout << "Found " << *found << '\n';
  }

  return 0;
}

Użytkownik może teraz wyszukiwać element naszej tablicy.

Wyjście

search for: nana
Found banana

Jak więc właściwie działają przechwytywania?

Chociaż może wyglądać na to, że nasza lambda w powyższym przykładzie uzyskuje bezpośredni dostęp do wartości main'S search zmiennej, to tak nie jest. Lambdy mogą wyglądać jak zagnieżdżone bloki, ale działają nieco inaczej (a rozróżnienie jest ważne).

Gdy wykonywana jest definicja lambdy, dla każdej zmiennej przechwytywanej przez lambdę tworzony jest klon tej zmiennej (o identycznej nazwie) wewnątrz lambdy. Te sklonowane zmienne są inicjowane w tym momencie ze zmiennych zakresu zewnętrznego o tej samej nazwie.

Tak więc w powyższym przykładzie, gdy tworzony jest obiekt lambda, lambda otrzymuje własną sklonowaną zmienną o nazwie search. Sklonowana search ma tę samą wartość co main'S search, więc zachowuje się, jakbyśmy uzyskiwali dostęp do main'S search, ale tak nie jest.

Chociaż te sklonowane zmienne mają tę samą nazwę, niekoniecznie mają ten sam typ co zmienna oryginalna. Zbadamy to w kolejnych częściach tej lekcji.

Kluczowa informacja

Przechwycone zmienne lambdy są kopiami zmiennych o zewnętrznym zasięgu, a nie rzeczywistych zmiennych.

Dla zaawansowanych czytelników

Chociaż lambdy wyglądają jak funkcje, w rzeczywistości są obiektami, które można nazwać funkcjami (są to zwane funktorami -- w przyszłej lekcji omówimy, jak utworzyć własne funktory od podstaw).

Kiedy kompilator napotyka definicję lambda, tworzy niestandardową definicję obiektu dla tej lambdy. Każda przechwycona zmienna staje się elementem danych obiektu.

W czasie wykonywania, po napotkaniu definicji lambda, tworzona jest instancja obiektu lambda i w tym momencie inicjowane są elementy składowe lambda.

Przechwycenia są domyślnie traktowane jako const

Gdy wywoływana jest lambda, wywoływane jest operator() . Domyślnie operator() traktuje przechwytywanie jako stałą, co oznacza, że lambda nie może modyfikować tych przechwytywania.

W poniższym przykładzie przechwytujemy zmienną ammo i próbujemy ją zmniejszyć.

#include <iostream>

int main()
{
  int ammo{ 10 };

  // Define a lambda and store it in a variable called "shoot".
  auto shoot{
    [ammo]() {
      // Illegal, ammo cannot be modified.
      --ammo;

      std::cout << "Pew! " << ammo << " shot(s) left.\n";
    }
  };

  // Call the lambda
  shoot();

  std::cout << ammo << " shot(s) left\n";

  return 0;
}

Powyższe nie zostanie skompilowane, ponieważ ammo jest traktowane jako stała w obrębie lambda.

Mutable przechwytuje

Aby umożliwić modyfikację przechwyconych zmiennych, możemy oznaczyć lambdę jako mutable:

#include <iostream>

int main()
{
  int ammo{ 10 };

  auto shoot{
    [ammo]() mutable { // now mutable
      // We're allowed to modify ammo now
      --ammo;

      std::cout << "Pew! " << ammo << " shot(s) left.\n";
    }
  };

  shoot();
  shoot();

  std::cout << ammo << " shot(s) left\n";

  return 0;
}

Wyjście:

Pew! 9 shot(s) left.
Pew! 8 shot(s) left.
10 shot(s) left

Podczas kompilacji nadal występuje błąd logiczny. Co się stało? Po wywołaniu lambda przechwyciła kopię z ammo. Kiedy lambda zmniejszała ammo z 10 Do 9 Do 8, zmniejszała swoją własną kopię, a nie oryginalną ammo wartość w main().

Zauważ, że wartość ammo jest zachowywana we wszystkich wywołaniach lambdy!

Ostrzeżenie

Ponieważ przechwycone zmienne są elementami obiektu lambda, ich wartości są utrwalane w wielu wywołaniach lambdy!

Przechwytywanie przez referencja

Podobnie jak funkcje mogą zmieniać wartość argumentów przekazywanych przez referencję, możemy także przechwytywać zmienne przez referencję, aby nasza lambda miała wpływ na wartość argumentu.

Aby przechwycić zmienną przez referencję, dodajemy ampersand (&) do nazwy zmiennej podczas przechwytywania. W przeciwieństwie do zmiennych przechwytywanych przez wartość, zmienne przechwytywane przez referencję nie są stałymi, chyba że przechwytywana zmienna to const. Przechwytywanie przez referencję powinno być preferowane zamiast przechwytywania przez wartość, gdy normalnie wolisz przekazać argument do funkcji przez referencję (np. w przypadku typów innych niż podstawowe).

Oto powyższy kod z ammo przechwyconym przez referencję:

#include <iostream>

int main()
{
  int ammo{ 10 };

  auto shoot{
    // We don't need mutable anymore
    [&ammo]() { // &ammo means ammo is captured by reference
      // Changes to ammo will affect main's ammo
      --ammo;

      std::cout << "Pew! " << ammo << " shot(s) left.\n";
    }
  };

  shoot();

  std::cout << ammo << " shot(s) left\n";

  return 0;
}

To daje oczekiwaną odpowiedź:

Pew! 9 shot(s) left.
9 shot(s) left

Teraz użyjmy przechwytywania referencji, aby policzyć, ile porównania std::sort wykonywane podczas sortowania tablicy.

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>

struct Car
{
  std::string_view make{};
  std::string_view model{};
};

int main()
{
  std::array<Car, 3> cars{ { { "Volkswagen", "Golf" },
                             { "Toyota", "Corolla" },
                             { "Honda", "Civic" } } };

  int comparisons{ 0 };

  std::sort(cars.begin(), cars.end(),
    // Capture @comparisons by reference.
    [&comparisons](const auto& a, const auto& b) {
      // We captured comparisons by reference. We can modify it without "mutable".
      ++comparisons;

      // Sort the cars by their make.
      return a.make < b.make;
  });

  std::cout << "Comparisons: " << comparisons << '\n';

  for (const auto& car : cars)
  {
    std::cout << car.make << ' ' << car.model << '\n';
  }

  return 0;
}

Możliwe wyniki

Comparisons: 2
Honda Civic
Toyota Corolla
Volkswagen Golf

Przechwytywanie wielu zmiennych

Wiele zmiennych można przechwycić, oddzielając je przecinkiem. Może to obejmować kombinację zmiennych przechwytywanych według wartości lub przez odniesienie:

int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};

// Capture health and armor by value, and enemies by reference.
[health, armor, &enemies](){};

Przechwytywanie domyślne

Konieczność jawnego wyszczególnienia zmiennych, które chcesz przechwycić, może być uciążliwa. Jeśli zmodyfikujesz lambdę, możesz zapomnieć o dodaniu lub usunięciu przechwyconych zmiennych. Na szczęście możemy skorzystać z pomocy kompilatora, aby automatycznie wygenerował listę zmiennych, które musimy przechwycić.

A default catch (zwana także capture-default) przechwytuje wszystkie zmienne wymienione w lambdzie. Zmienne niewymienione w lambdzie nie są przechwytywane, jeśli używane jest przechwytywanie domyślne.

Aby przechwycić wszystkie używane zmienne według wartości, użyj wartości przechwytywania =.
Aby przechwycić wszystkie używane zmienne przez odniesienie, użyj wartości przechwytywania &.

Oto przykład użycia domyślnego przechwytywania według wartości:

#include <algorithm>
#include <array>
#include <iostream>

int main()
{
  std::array areas{ 100, 25, 121, 40, 56 };

  int width{};
  int height{};

  std::cout << "Enter width and height: ";
  std::cin >> width >> height;

  auto found{ std::find_if(areas.begin(), areas.end(),
                           [=](int knownArea) { // will default capture width and height by value
                             return width * height == knownArea; // because they're mentioned here
                           }) };

  if (found == areas.end())
  {
    std::cout << "I don't know this area :(\n";
  }
  else
  {
    std::cout << "Area found :)\n";
  }

  return 0;
}

Przechwytywanie domyślne można łączyć z normalnymi przechwytywaniami. Niektóre zmienne możemy przechwycić według wartości, a inne przez referencję, ale każdą zmienną można przechwycić tylko raz.

int health{ 33 };
int armor{ 100 };
std::vector<CEnemy> enemies{};

// Capture health and armor by value, and enemies by reference.
[health, armor, &enemies](){};

// Capture enemies by reference and everything else by value.
[=, &enemies](){};

// Capture armor by value and everything else by reference.
[&, armor](){};

// Illegal, we already said we want to capture everything by reference.
[&, &armor](){};

// Illegal, we already said we want to capture everything by value.
[=, armor](){};

// Illegal, armor appears twice.
[armor, &health, &armor](){};

// Illegal, the default capture has to be the first element in the capture group.
[armor, &](){};

Definiowanie nowych zmiennych w przechwytywaniu lambda

Czasami chcemy przechwycić zmienną z niewielką modyfikacją lub zadeklarować nową zmienną, która jest widoczna tylko w zakresie lambda. Możemy to zrobić definiując zmienną w przechwytywaniu lambda bez określania jej typu.

#include <array>
#include <iostream>
#include <algorithm>

int main()
{
  std::array areas{ 100, 25, 121, 40, 56 };

  int width{};
  int height{};

  std::cout << "Enter width and height: ";
  std::cin >> width >> height;

  // We store areas, but the user entered width and height.
  // We need to calculate the area before we can search for it.
  auto found{ std::find_if(areas.begin(), areas.end(),
                           // Declare a new variable that's visible only to the lambda.
                           // The type of userArea is automatically deduced to int.
                           [userArea{ width * height }](int knownArea) {
                             return userArea == knownArea;
                           }) };

  if (found == areas.end())
  {
    std::cout << "I don't know this area :(\n";
  }
  else
  {
    std::cout << "Area found :)\n";
  }

  return 0;
}

userArea zostanie obliczone tylko raz, gdy zdefiniowana zostanie lambda. Obliczony obszar jest przechowywany w obiekcie lambda i jest taki sam dla każdego wywołania. Jeśli lambda jest zmienna i modyfikuje zmienną zdefiniowaną podczas przechwytywania, pierwotna wartość zostanie nadpisana.

Najlepsza praktyka

Inicjuj zmienne w przechwytywaniu tylko wtedy, gdy ich wartość jest krótka, a ich typ jest oczywisty. W przeciwnym razie najlepiej zdefiniować zmienną poza lambdą i przechwycić ją.

Wiszące przechwycone zmienne

Zmienne są przechwytywane w momencie zdefiniowania lambdy. Jeśli zmienna przechwycona przez referencję umrze przed lambdą, lambda pozostanie z zawieszoną referencją.

Na przykład:

#include <iostream>
#include <string>

// returns a lambda
auto makeWalrus(const std::string& name)
{
  // Capture name by reference and return the lambda.
  return [&]() {
    std::cout << "I am a walrus, my name is " << name << '\n'; // Undefined behavior
  };
}

int main()
{
  // Create a new walrus whose name is Roofus.
  // sayName is the lambda returned by makeWalrus.
  auto sayName{ makeWalrus("Roofus") };

  // Call the lambda function that makeWalrus returned.
  sayName();

  return 0;
}

Wywołanie makeWalrus() tworzy tymczasowy std::string z literału łańcuchowego "Roofus". Lambda w makeWalrus() przechwytuje ciąg tymczasowy przez odniesienie. Łańcuch tymczasowy umiera na końcu pełnego wyrażenia zawierającego wywołanie makeWalrus(), ale lambda sayName w dalszym ciągu odwołuje się do niego poza tym punktem. Zatem, gdy wywołujemy sayName, uzyskujemy dostęp do wiszącego odniesienia, co powoduje niezdefiniowane zachowanie.

Zauważ, że dzieje się to również w przypadku "Roofus" jest przekazywane do makeWalrus() według wartości. Parametr name umiera na końcu makeWalrus(), a lambda pozostaje z zawieszoną referencją.

Ostrzeżenie

Zachowaj szczególną ostrożność podczas przechwytywania zmiennych przez referencję, szczególnie w przypadku domyślnego przechwytywania referencji. Przechwycone zmienne muszą przetrwać dłużej niż lambda.

Jeśli chcemy, aby przechwycone name były prawidłowe, gdy używana jest lambda, musimy zamiast tego przechwycić je według wartości (albo jawnie, albo przy użyciu domyślnego przechwytywania według wartości).

Niezamierzone kopie modyfikowalnych lambd

Ponieważ lambdy są obiektami, można je kopiować. W niektórych przypadkach może to powodować problemy. Rozważ następujący kod:

#include <iostream>

int main()
{
  int i{ 0 };

  // Create a new lambda named count
  auto count{ [i]() mutable {
    std::cout << ++i << '\n';
  } };

  count(); // invoke count

  auto otherCount{ count }; // create a copy of count

  // invoke both count and the copy
  count();
  otherCount();

  return 0;
}

Wyjście

1
2
2

Zamiast drukować 1, 2, 3, kod drukuje 2 dwukrotnie. Kiedy tworzyliśmy otherCount jako kopię count, stworzyliśmy kopię count w jej obecnym stanie. count'S i wyniosło 1, więc otherCount'S i jest również 1. Ponieważ otherCount jest kopią count każdy z nich ma swój własny i.

Przyjrzyjmy się teraz nieco mniej oczywistemu przykładowi:

#include <iostream>
#include <functional>

void myInvoke(const std::function<void()>& fn)
{
    fn();
}

int main()
{
    int i{ 0 };

    // Increments and prints its local copy of @i.
    auto count{ [i]() mutable {
      std::cout << ++i << '\n';
    } };

    myInvoke(count);
    myInvoke(count);
    myInvoke(count);

    return 0;
}

Wyjście:

1
1
1

Wykazuje ten sam problem, co poprzedni przykład w bardziej niejasnej formie.

Kiedy wywołujemy myInvoke(count), kompilator zobaczy, że count (który ma typ lambda) nie pasuje do typu parametru referencyjnego (std::function<void()>). Przekonwertuje lambdę na tymczasową std::function , tak aby parametr referencyjny mógł się z nią powiązać, co spowoduje utworzenie kopii lambdy. Zatem nasze wywołanie fn() jest faktycznie wykonywane na kopii naszej lambdy, która istnieje jako część tymczasowej std::function, a nie rzeczywistej lambdy.

Jeśli musimy przekazać zmienną lambdę i chcemy uniknąć możliwości utworzenia przypadkowych kopii, istnieją dwie możliwości. Jedną z opcji jest użycie zamiast tego lambdy nieprzechwytującej — w powyższym przypadku moglibyśmy usunąć przechwytywanie i śledzić nasz stan, używając zamiast tego statycznej zmiennej lokalnej. Jednak statyczne zmienne lokalne mogą być trudne do śledzenia i sprawić, że nasz kod będzie mniej czytelny. Lepszą opcją jest przede wszystkim uniemożliwienie tworzenia kopii naszej lambdy. Ale skoro nie możemy wpływać na sposób implementacji std::function (lub innych standardowych funkcji bibliotecznych lub obiektów), jak możemy to zrobić?

Jedną z opcji (h/t do czytnika Dck) jest natychmiastowe umieszczenie naszej lambdy w std::function . W ten sposób, gdy wywołamy myInvoke(), parametr referencyjny fn może powiązać się z naszym std::function i nie zostanie utworzona żadna tymczasowa kopia:

#include <iostream>
#include <functional>

void myInvoke(const std::function<void()>& fn)
{
    fn();
}

int main()
{
    int i{ 0 };

    // Increments and prints its local copy of @i.
    std::function count{ [i]() mutable { // lambda object stored in a std::function
      std::cout << ++i << '\n';
    } };

    myInvoke(count); // doesn't create copy when called
    myInvoke(count); // doesn't create copy when called
    myInvoke(count); // doesn't create copy when called

    return 0;
}

Nasze dane wyjściowe są teraz zgodne z oczekiwaniami:

1
2
3

Alternatywnym rozwiązaniem jest użycie opakowania referencyjnego. C++ udostępnia wygodny typ (jako część nagłówka <function>) o nazwie std::reference_wrapper , który pozwala nam przekazywać normalny typ tak, jakby był referencją. Dla jeszcze większej wygody std::reference_wrapper można utworzyć za pomocą funkcji std::ref() . Zawijając naszą lambdę w std::reference_wrapper, za każdym razem, gdy ktoś spróbuje utworzyć kopię naszej lambdy, zamiast tego utworzy kopię reference_wrapper (unikając tworzenia kopii lambdy).

Oto nasz zaktualizowany kod przy użyciu std::ref:

#include <iostream>
#include <functional> // includes std::reference_wrapper and std::ref

void myInvoke(const std::function<void()>& fn)
{
    fn();
}

int main()
{
    int i{ 0 };

    // Increments and prints its local copy of @i.
    auto count{ [i]() mutable {
      std::cout << ++i << '\n';
    } };

    // std::ref(count) ensures count is treated like a reference
    // thus, anything that tries to copy count will actually copy the reference
    // ensuring that only one count exists
    myInvoke(std::ref(count));
    myInvoke(std::ref(count));
    myInvoke(std::ref(count));

    return 0;
}

Nasze dane wyjściowe są teraz zgodne z oczekiwaniami:

1
2
3

Interesujące w tej metodzie jest to, że działa ona nawet jeśli myInvoke przyjmuje fn przez wartość (a nie przez odniesienie)!

Reguła

Funkcje z bibliotek standardowych mogą kopiować obiekty funkcyjne (przypomnienie: lambdy są obiektami funkcyjnymi). Jeśli chcesz dostarczyć lambdy z modyfikowalnymi, przechwyconymi zmiennymi, przekaż je przez referencję, używając std::ref.

Najlepsza praktyka

Staraj się unikać modyfikowalnych lambd. Niemodyfikowalne lambdy są łatwiejsze do zrozumienia i nie są obarczone powyższymi problemami, a także bardziej niebezpiecznymi problemami, które pojawiają się po dodaniu wykonania równoległego.

Czas quizu

Pytanie nr 1

Która z poniższych zmiennych może zostać użyta przez lambdę w main bez ich jawnego przechwytywania?

int i{};
static int j{};

int getValue()
{
  return 0;
}

int main()
{
  int a{};
  constexpr int b{};
  static int c{};
  static constexpr int d{};
  const int e{};
  const int f{ getValue() };
  static const int g{}; 
  static const int h{ getValue() }; 

  [](){
    // Try to use the variables without explicitly capturing them.
    a;
    b;
    c;
    d;
    e;
    f;
    g;
    h;
    i;
    j;
  }();

  return 0;
}

Pokaż rozwiązanie

Pytanie nr 2

Co wypisuje poniższy kod? Nie uruchamiaj kodu, przemyśl go w swojej głowie.

#include <iostream>
#include <string>

int main()
{
  std::string favoriteFruit{ "grapes" };

  auto printFavoriteFruit{
    [=]() {
      std::cout << "I like " << favoriteFruit << '\n';
    }
  };

  favoriteFruit = "bananas with chocolate";

  printFavoriteFruit();

  return 0;
}

Pokaż rozwiązanie

Pytanie nr 3

Napiszemy małą grę z liczbami kwadratowymi (liczbami, które można utworzyć mnożąc liczbę całkowitą przez siebie (1, 4, 9, 16, 25, …)).

Aby skonfigurować grę:

  • Poproś użytkownika o wprowadzenie liczby, od której zaczniesz (np. 3).
  • Zapytaj użytkownika, ile wartości ma wygenerować.
  • Wybierz losową liczbę całkowitą z zakresu od 2 do 4. To jest mnożnik.
  • Wygeneruj liczbę wartości wskazaną przez użytkownika. Zaczynając od numeru początkowego, każda wartość powinna być następną liczbą kwadratową pomnożoną przez mnożnik.

Aby zagrać w grę:

  • Użytkownik wprowadza typ.
  • Jeśli odgadnięcie pasuje do dowolnej wygenerowanej wartości, wartość jest usuwana z listy, a użytkownik może zgadnąć ponownie.
  • Jeśli użytkownik odgadnie wszystkie wygenerowane wartości, zostaną one wygrana.
  • Jeśli odgadnięcie nie pasuje do wygenerowanej wartości, użytkownik przegrywa, a program podaje mu najbliższą nieprzewidzianą wartość.

Oto kilka przykładowych sesji, które pozwolą ci lepiej zrozumieć, jak działa gra:

Start where? 4
How many? 5
I generated 5 square numbers. Do you know what each number is after multiplying it by 2?
> 32
Nice! 4 number(s) left.
> 72
Nice! 3 number(s) left.
> 50
Nice! 2 number(s) left.
> 126
126 is wrong! Try 128 next time.
  • Począwszy od 4, program generuje kolejnych 5 kwadratów: 16, 25, 36, 49, 64
  • Program wybrał 2 jako losowy mnożnik, więc każdy kwadrat jest mnożony przez 2: 32, 50, 72, 98, 128
  • Teraz użytkownik może zgadnąć.
  • 32 jest na liście.
  • 72 jest na liście.
  • 126 nie ma na liście, więc użytkownik traci. Najbliższa nieodgadniona liczba to 128.
Start where? 1
How many? 3
I generated 3 square numbers. Do you know what each number is after multiplying it by 4?
> 4
Nice! 2 number(s) left.
> 16
Nice! 1 number(s) left.
> 36
Nice! You found all numbers, good job!
  • Poczynając od 1, program generuje kolejne 3 kwadraty: 1, 4, 9
  • Program wybrał 4 jako losowy mnożnik, więc każdy kwadrat jest mnożony przez 4: 4, 16, 36
  • Użytkownik poprawnie odgadnie wszystkie liczby i wygrywa gra.

Wskazówki:

auto found{ std::find(/* ... */) };

// Make sure the element was found

myVector.erase(found);
  • Użyj std::min_element i lambdę, aby znaleźć liczbę najbliższą użytkownikowi zgadnij. std::min_element działa analogicznie do std::max_element z poprzedniego quizu.

Pokaż wskazówkę

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
485 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze