В тази публикация проучихме използването на std::visit в C++, което е мощна помощна програма, която ни позволява да прилагаме различни функции към вариантен обект въз основа на текущия му тип. std::visit може да бъде полезен в сценарии, при които трябва да обработваме вариантни обекти с множество възможни типове, като например в парсер, където може да се наложи да обработваме различни типове входни данни по различен начин. За да разберем какво постига std::visit, разгледахме алтернативен начин за постигане на същата функционалност, използвайки оператори if-else и претоварване на функции.

Прост пример

Ето прост типичен пример за итерация през масив от варианти на 3 различни типа и извикващи функции, претоварени за всеки тип:

#include <iostream>
#include <variant>
#include <vector>

void func(int i) {
  std::cout << "Called func(int): " << i << std::endl;
}

void func(double d) {
  std::cout << "Called func(double): " << d << std::endl;
}

void func(const std::string& s) {
  std::cout << "Called func(string): " << s << std::endl;
}

int main() {
  std::vector<std::variant<int, double, std::string>> myVector = {1, 3.14, "Hello"};

  for (auto& element : myVector) {
    std::visit([](auto&& arg){ func(arg); }, element);
  }

  return 0;
}

В този пример дефинираме три претоварени функции: func(int), func(double) и func(const std::string&). След това създаваме вектор от std::variant<int, double, std::string> и го инициализираме с три елемента от различни типове.

В цикъла for ние използваме std::visit, за да извикаме подходящото претоварване на функцията за всеки елемент във вектора. std::visit приема извикваем обект като свой първи аргумент, който в този случай е ламбда функция, която извиква func със своя аргумент. Синтаксисът auto&& във функцията ламбда казва на компилатора да изведе типа на аргумента и да създаде препратка към него, което ни позволява да извикаме func с правилното претоварване за всеки тип вариант.

std::visit е полиморфна функция по време на изпълнение, която определя типа на вариантния обект по време на изпълнение и след това извиква претоварване на съответната функция.

Когато извикаме std::visit, ние му предаваме извикваем обект, който дефинира набор от претоварени функционални обекти за всеки тип във варианта. По време на изпълнение std::visit използва типа на вариантния обект, за да определи кой функционален обект да извика, и след това предава вариантния обект на този функционален обект.

Така че в примера, който предоставих, ламбда функцията, предадена на std::visit, е обект на посетител, който дефинира набора от претоварени func функции за всеки тип във варианта. По време на изпълнение std::visit определя типа на вариантния обект във вектора и след това извиква подходящата func функция за този тип.

Как std::visit знае типа на вариантния обект?

std::visit използва информацията за типа, съхранена в самия вариантен обект, за да определи неговия тип по време на изпълнение. Когато създаваме вариантен обект, той съдържа стойност на един от неговите алтернативни типове, заедно с дискриминатор, който показва кой тип притежава в момента.
Вариантният обект съхранява този дискриминатор вътрешно, така че когато извикаме std::visit на варианта обект, той инспектира този дискриминатор, за да определи типа на обекта.

След като std::visit знае типа на обекта, той избира съответния функционален обект, който да извика от набора от претоварени функционални обекти, предоставени от посетителя. След това извиква този функционален обект, като предава вариантния обект като аргумент.

Така че в нашия пример, когато извикаме std::visit на всеки елемент във вектора, той проверява дискриминатора на вариантния обект, за да определи неговия тип, и след това извиква подходящата func функция за този тип.

Този еквивалентен код без използване на претоварване на посещения и функции може да обясни повече какво прави концептуално std::visit.

#include <iostream>
#include <vector>
#include <variant>

void func(int i) {
    std::cout << "Called func(int): " << i << std::endl;
}

void func(double d) {
    std::cout << "Called func(double): " << d << std::endl;
}

void func(const std::string& s) {
    std::cout << "Called func(string): " << s << std::endl;
}

int main() {
    std::vector<std::variant<int, double, std::string>> vec = {1, 3.14, "hello"};

    for (const auto& variant : vec) {
        if (std::holds_alternative<int>(variant)) {
            int value = std::get<int>(variant);
            func(value);
        }
        else if (std::holds_alternative<double>(variant)) {
            double value = std::get<double>(variant);
            func(value);
        }
        else if (std::holds_alternative<std::string>(variant)) {
            std::string value = std::get<std::string>(variant);
            func(value);
        }
    }

    return 0;
}

В този пример дефинираме три претоварени функции: func(int), func(double) и func(const std::string&).

След това създаваме вектор от вариантни обекти, vec, който съдържа по един елемент от всеки тип: int, double и std::string.

След това итерираме всеки елемент в vec, като използваме std::holds_alternative, за да определим типа на текущия вариантен обект. Използваме std::get, за да извлечем стойността на вариантния обект и след това предаваме тази стойност на подходящата функция въз основа на нейния тип.

Резюме

В обобщение, проучихме използването на std::visit в C++, което е мощна помощна програма, която ни позволява да прилагаме различни функции към вариантен обект въз основа на текущия му тип. Като цяло публикацията в блога цели да помогне на читателите да разберат основите на std::visit и как може да се използва за безопасно преминаване през различни обекти в C++.

Кодиране на ниво нагоре

Благодарим ви, че сте част от нашата общност! Преди да тръгнеш:

🚀👉 Присъединете се към колектива за таланти Level Up и намерете невероятна работа