Избавляемся от лишнего кода при использовании std::variant

Доброе утро!

Для решения некоторых задач возникает потребность хранить в одном месте объекты разных типов. В 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 для объектов с одинаковым интерфейсом, а так же научились параметризовывать такие вызовы.