Реализация оператора in в С++
Привет! Сегодня я надеюсь показать вам немного магии. Моим хобби является придумывание всяких казалось бы невозможных штук на С++, что помогает мне в изучении всевозможных тонкостей языка ну или просто развлечься. Оператор in есть в нескольких языках, например Python, JS. Но в С++ его не завезли, но иногда хочется чтобы он был, так почему бы его не реализовать.
std::unordered_map some_map =
{
{ "black", "white" },
{ "cat", "dog" },
{ "day", "night" }
};
if (auto res = "cat" in some_map)
{
res->second = "fish";
}
Как должен работать оператор я полагаю очевидно. Он берёт левый объект и проверяет есть ли вхождение этого объекта в объекте указанном справа, который не обязательно должен быть коллекцией. Само собой универсального решения нет, как нет универсального решения и для других операторов, потому и была придумана возможность перегрузить их. Следовательно и для оператора in нужно реализовать подобный механизм.
Перегрузка будет выглядеть, например так.
bool operator_in(const string& key, const unordered_map& data)
{
return data.find(key) != data.end();
}
Думаю мысль ясна, выражение вида.
"some string" in some_map
Должно превратится в вызов функции.
operator_in("some string", some_map)
Реализовать данный механизм довольно просто, используя существующие возможности по перегрузке операторов. Сам оператор in по сути своей является макросом который делает перемножение.
#define in *OP_IN_HELP{}*
В данном случае OP_IN_HELP является пустым классом и служит нам только для того чтобы выбрать правильную перегрузку.
class OP_IN_HELP
{};
template
OP_IN_LVAL operator*(const TIn& data, const OP_IN_HELP&)
{
return OP_IN_LVAL(data);
}
Оператор является шаблонным, что позволяет в качестве первого аргумента принимать любой тип. Теперь нам нужно как-то получить правый объект, не потеряв при этом левый. Для этого мы реализуем класс OP_IN_LVAL который будет хранить наш левый объект.
template
struct OP_IN_LVAL
{
const TIn& m_in;
OP_IN_LVAL(const TIn& val) : m_in(val)
{};
};
Так как сам объект будет жив пока выполняется выражение, то нет ничего страшного если мы будем хранить константную ссылку на этот объект. Теперь нам остаётся лишь реализовать внутренний оператор перемножения, который будет нам возвращать результат выполнения перегруженного оператора in, само собой он будет шаблонным.
template
struct OP_IN_LVAL
{
const TIn& m_in;
OP_IN_LVAL(const TIn& val) : m_in(val)
{};
template
bool operator*(const TWhat& what) const
{
return operator_in(m_in, what);
}
};
Собственно это решение уже будет работать, но оно является ограниченным и не позволит нам писать так.
if (auto res = "true" in some_map)
{
res->second = "false";
}
Для того чтобы мы имели такую возможность, нам необходимо прокидывать возвращаемое значение перегруженного оператора. Есть две версии как это сделать, одна использует возможности с++14, друга работает в рамках с++11.
template
struct OP_IN_LVAL
{
const TIn& m_in;
OP_IN_LVAL(const TIn& val)
:m_in(val)
{};
// Версия для C++14
template
auto operator*(const TWhat& what) const
{
return operator_in(m_in, what);
}
// Версия для C++11
template
auto operator*(const TWhat& what) const -> decltype(operator_in(m_in, what))
{
return operator_in(m_in, what);
}
// Для мутабельных объектов нам нужен оператор
// принимающий объект по не константной ссылке
template
auto operator*(TWhat& what) const -> decltype(operator_in(m_in, what))
{
return operator_in(m_in, what);
}
};
Так как я в основном работаю в Visual Studio 2013 я ограничен рамками С++11 к тому же решение в рамках С++11 будет успешно работать и в С++14, потому советую выбирать его.
Пример реализации обобщённого оператора in для unordered_map.
template
class OpInResult
{
bool m_result;
TIterator m_iter;
public:
OpInResult(bool result, TIterator& iter)
: m_result(result), m_iter(iter)
{}
operator bool()
{
return m_result;
}
TIterator& operator->()
{
return m_iter;
}
TIterator& data()
{
return m_iter;
}
};
template
auto operator_in(const TKey& key, std::unordered_map& data) ->
OpInResult::iterator>
{
auto iter = data.find(key);
return OpInResult::iterator>(iter != data.end(), iter);
}
template
auto operator_in(const char* key, std::unordered_map& data) ->
OpInResult::iterator>
{
auto iter = data.find(key);
return OpInResult::iterator>(iter != data.end(), iter);
}
Класс OpInResult позволяет переопределяет оператор приведения типа, что позволяет нам использовать его в if. А также переопределяет стрелочный оператор, что позволяет маскировать себя под итератор который возвращает unordered_map.find ().
Пример можно посмотреть тут cpp.sh/7rfdw
Хотелось бы также сказать о некоторых особенностях данного решения.
Visual Studio инстанцирует шаблонны в месте использования, что означает что сама функция перегрузки должна быть объявлена до места использования оператора, но может быть объявлена после декларации класса OP_IN_LVAL. GCC в свою очередь инстанцирует шаблон в месте объявления (когда встречает использование само собой), что означает, что перегруженный оператор должен быть объявлен до того как декларируется класс OP_IN_LVAL. Если не совсем понятно о чём речь, то вот пример. cpp.sh/5jxcq В этом коде я всего лишь перенёс перегрузку оператора in ниже декларации класса OP_IN_LVAL и он перестал компилироваться в GCC (если только не компилировать с флагом -fpermissive), но успешно компилируется в Visual Studio.
В С++20 должна появится возможность писать так.
if (auto res = some_map.find("true"); res != some_map.end())
{
res->second = "false";
}
Но как мне кажется конструкция вида
if (auto res = "true" in some_map)
{
res->second = "false";
}
Выглядит приятнее.
Больше примеров перегрузок можно увидеть тут github.com/ChaosOptima/operator_in
На основе принципа реализации данного оператора также не составит проблем реализовать
и другие операторы и выражения, например.
negative = FROM some_vector WHERE [](int x){return x < 0;};
null-conditional operator
auto result = $if some_ptr $->func1()$->func2()$->func3(10, 11, 0)$endif;
patern matching
succes = patern_match val
with_type(int x)
{
cout << "some int " << x << '\n';
}
with_cast(const std::vector& items)
{
for (auto&& val : items)
std::cout << val << ' ';
std::cout << '\n';
}
with(std::string()) [&](const std::string&)
{
cout << "empty string\n";
}
with(oneof("one", "two", "three")) [&](const std::string& value)
{
cout << value << "\n";
}
with_cast(const std::string& str)
{
cout << "some str " << str << '\n';
}
at_default
{
cout << "no match";
};
string enum
StringEnum Power $def
(
POW0,
POW1,
POW2 = POW1 * 2,
POW3,
POW4 = POW3 + 1,
POW8 = POW4 * 2,
POW9,
POW10
);
to_string(Power::POW0)
from_string("POW0")