📜 ⬆️ ⬇️

The implementation of the in operator in C ++

Hello! Today I hope to show you some magic. My hobby is inventing all sorts of seemingly impossible pieces in C ++, which helps me in learning all sorts of subtleties of the language or just have fun. The in operator is available in several languages, for example, Python, JS. But in C ++ it was not delivered, but sometimes you want it to be, so why not implement it.

std::unordered_map<std::string, std::string> some_map = { { "black", "white" }, { "cat", "dog" }, { "day", "night" } }; if (auto res = "cat" in some_map) { res->second = "fish"; } 


How should the operator work, I suppose obviously. It takes the left object and checks whether there is an occurrence of this object in the object specified on the right, which does not have to be a collection. It goes without saying that there is no universal solution, just as there is no universal solution for other operators, and therefore it was possible to overload them. Consequently, the in operator also needs to implement a similar mechanism.

Overload will look like this.
')
 bool operator_in(const string& key, const unordered_map<string, string>& data) { return data.find(key) != data.end(); } 

I think the idea is clear, the expression of the form.

  "some string" in some_map 

It should turn into a function call.

  operator_in("some string", some_map) 

Implementing this mechanism is quite simple, using existing opportunities for operator overloading. The in operator itself is essentially a macro that does multiplication.

  #define in *OP_IN_HELP{}* 

In this case, OP_IN_HELP is an empty class and only serves us to select the correct overload.

  class OP_IN_HELP {}; template<class TIn> OP_IN_LVAL<TIn> operator*(const TIn& data, const OP_IN_HELP&) { return OP_IN_LVAL<TIn>(data); } 

The operator is a template that allows you to take any type as the first argument. Now we need to somehow get the right object, without losing the left. To do this, we will implement the class OP_IN_LVAL which will store our left object.

  template<class TIn> struct OP_IN_LVAL { const TIn& m_in; OP_IN_LVAL(const TIn& val) : m_in(val) {}; }; 

Since the object itself will be alive while the expression is executed, there is nothing to worry about if we keep a constant reference to this object. Now we just have to implement the internal multiplication operator, which will return the result of the overloaded operator in, it will by itself be a template.

  template<class TIn> struct OP_IN_LVAL { const TIn& m_in; OP_IN_LVAL(const TIn& val) : m_in(val) {}; template<class TWhat> bool operator*(const TWhat& what) const { return operator_in(m_in, what); } }; 

Actually this decision will already work, but it is limited and will not allow us to write like that.

  if (auto res = "true" in some_map) { res->second = "false"; } 

In order for us to have such an opportunity, we need to return the return value of the overloaded operator. There are two versions of how to do this, one uses the capabilities of c ++ 14, a friend works within the framework of c ++ 11.

  template<class TIn> struct OP_IN_LVAL { const TIn& m_in; OP_IN_LVAL(const TIn& val) :m_in(val) {}; //   C++14 template<class TWhat> auto operator*(const TWhat& what) const { return operator_in(m_in, what); } //   C++11 template<class TWhat> auto operator*(const TWhat& what) const -> decltype(operator_in(m_in, what)) { return operator_in(m_in, what); } //       //       template<class TWhat> auto operator*(TWhat& what) const -> decltype(operator_in(m_in, what)) { return operator_in(m_in, what); } }; 

Since I mainly work in Visual Studio 2013, I am limited to C ++ 11, and the C ++ 11 solution will also work successfully in C ++ 14, so I advise you to choose it.

An example implementation of a generic in operator for unordered_map.

 template<class TIterator> 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<class TKey, class TVal> auto operator_in(const TKey& key, std::unordered_map<TKey, TVal>& data) -> OpInResult<typename std::unordered_map<TKey, TVal>::iterator> { auto iter = data.find(key); return OpInResult<typename std::unordered_map<TKey, TVal>::iterator>(iter != data.end(), iter); } template<class TKey, class TVal> auto operator_in(const char* key, std::unordered_map<TKey, TVal>& data) -> OpInResult<typename std::unordered_map<TKey, TVal>::iterator> { auto iter = data.find(key); return OpInResult<typename std::unordered_map<TKey, TVal>::iterator>(iter != data.end(), iter); } 

The OpInResult class allows you to override the cast operator, which allows us to use it in if. It also overrides the switch operator, which allows you to mask yourself as an iterator that returns unordered_map.find ().

An example can be found here cpp.sh/7rfdw

I would also like to say about some features of this solution.
Visual Studio instantiates the template at the place of use, which means that the overload function itself must be declared before the place where the operator is used, but can be declared after the OP_IN_LVAL class declaration . GCC, in turn, instantiates the template at the declaration location (when it encounters usage itself), which means that the overloaded statement must be declared before the OP_IN_LVAL class is declared . If it is not quite clear what this is about, then here is an example. cpp.sh/5jxcq In this code, I just transferred the overload of the operator in below the declaration of the class OP_IN_LVAL and it stopped compiling in GCC (unless compiled with the -fpermissive flag), but successfully compiled in Visual Studio.

In C ++ 17, it was possible to write like this.

  if (auto res = some_map.find("true"); res != some_map.end()) { res->second = "false"; } 

But it seems to me the construction of the form

  if (auto res = "true" in some_map) { res->second = "false"; } 

Looks nicer.

More examples of overloads can be seen here github.com/ChaosOptima/operator_in

On the basis of the implementation principle of this operator, it’s also easy
and other operators and expressions, for example.

  negative = FROM some_vector WHERE [](int x){return x < 0;}; 

PS
I would like to know if you are interested in such topics, does it make sense to write about it here? And would you like to know how to implement other interesting things?

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<int>& items) { for (auto&& val : items) std::cout << val << ' '; std::cout << '\n'; } with(std::string()) [&](const std::string&) { cout << "empty string\n"; } with(oneof<std::string>("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<Power>("POW0") 

Source: https://habr.com/ru/post/419579/


All Articles