Доброе утро!
Для решения некоторых задач возникает потребность хранить в одном месте объекты разных типов. В C++17 для её удовлетворения из Boost'а в std была стырезжина структура данных std::variant. Это контейнер, шаблонизируемый типами, которые он может хранить. Одним из вариантов доступа к элементам такого контейнера предлагается std::visit — штука, способная натравливать некоторый функтор на std::variant, который должен уметь разобраться с его содержимым.
Посмотрим, как можно применить этот подход (пример вдохновлён https://en.cppreference.com/w/cpp/utility/variant/visit):
#include <iostream>
#include <string>
#include <variant>
#include <vector>
struct a {
int x;
a(int x)
: x{x}
{ }
void operator()() const {
std::cout << "a(" << x << ")\n";
}
};
struct b {
std::string y;
b(std::string y)
: y{y}
{ }
void operator()() const {
std::cout << "b[" << y << "]\n";
}
};
using action_t = std::variant<a, b>;
struct executor {
void operator()(const a &obj) {
obj();
}
void operator()(const b &obj) {
obj();
}
};
void f(const std::vector<action_t> &actions) {
for (auto it = actions.begin(); it != actions.end(); ++it) {
std::visit(executor{}, *it);
}
}
int main(int c, char **v) {
std::vector<action_t> actions;
for (int i = 1; i < c; ++i) {
try {
actions.push_back(a{std::stoi(v[i])});
} catch (const std::invalid_argument &) {
actions.push_back(b{v[i]});
}
}
f(actions);
return 0;
}
% g++ --std=c++17 -Wall -Wextra -pedantic visit.cxx
% ./a.out ojn 13 nfo1 239 nd nff -3
b[ojn]
a(13)
b[nfo1]
a(239)
b[nd]
b[nff]
a(-3)
Супер, не правда ли? Что здесь происходит: мы объявили две структуры; создали список из вариантов этих структур; в зависимости от входных данных помещаем в список соответствующую структуру; проходимся по списку и вызываем operator() утилитарной структуры, перегруженный для каждой структуры.
Что можно упростить? В нашем случае можно отказаться от перегрузок под каждую структуру. Для этого достаточно, чтобы у всех структур был общий интерфейс. Тогда можно немножко шаблонизировать утилитарную структуру, используемую std::visit, получив следующее решение:
struct executor {
template<typename T>
void operator()(const T &obj) {
obj();
}
};
Таким образом, не меняя остального кода (при условии общего интерфейса) мы можем избавиться от перегрузок для каждого типа в std::variant<>.
Теперь предположим, что мы хотим немного параметризовать наши вызовы. Пусть функция, работающая со списком будет задавать скобки, в которые ограняется параметр при выводе. Тогда весь код превратится в нечто подобное:
#include <iostream>
#include <string>
#include <variant>
#include <vector>
struct a {
int x;
a(int x)
: x{x}
{ }
void operator()(char left, char right) const {
std::cout << "a" << left << x << right << "\n";
}
};
struct b {
std::string y;
b(std::string y)
: y{y}
{ }
void operator()(char left, char right) const {
std::cout << "b" << left << y << right << "\n";
}
};
using action_t = std::variant<a, b>;
struct executor {
char left;
char right;
explicit executor(char left, char right)
: left{left},
right{right}
{}
template<typename T>
void operator()(const T &obj) {
obj(left, right);
}
};
void f(const std::vector<action_t> &actions) {
std::vector<const char *> braces;
braces.push_back("()");
braces.push_back("[]");
braces.push_back("{}");
braces.push_back("<>");
for (std::size_t i = 0; i != actions.size(); ++i) {
std::visit(executor{braces[i%braces.size()][0],
braces[i%braces.size()][1]}, actions[i]);
}
}
int main(int c, char **v) {
std::vector<action_t> actions;
for (int i = 1; i < c; ++i) {
try {
actions.push_back(a{std::stoi(v[i])});
} catch (const std::invalid_argument &) {
actions.push_back(b{v[i]});
}
}
f(actions);
return 0;
}
Взглянем на вывод:
% g++ --std=c++17 -Wall -Wextra -pedantic visit.cxx
% ./a.out ojn 13 nfo1 239 nd nff -3
b(ojn)
a[13]
b{nfo1}
a<239>
b(nd)
b[nff]
a{-3}
То, что надо.
Резюмируя, в данной статье мы увидели нехитрый трюк, позволяющий избавиться от переопределения множества методов при использовании std::visit для объектов с одинаковым интерфейсом, а так же научились параметризовывать такие вызовы.