Реализация оператора in в С++

    Привет! Сегодня я надеюсь показать вам немного магии. Моим хобби является придумывание всяких казалось бы невозможных штук на С++, что помогает мне в изучении всевозможных тонкостей языка ну или просто развлечься. Оператор in есть в нескольких языках, например Python, JS. Но в С++ его не завезли, но иногда хочется чтобы он был, так почему бы его не реализовать.

    	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";
    	}
    


    Как должен работать оператор я полагаю очевидно. Он берёт левый объект и проверяет есть ли вхождение этого объекта в объекте указанном справа, который не обязательно должен быть коллекцией. Само собой универсального решения нет, как нет универсального решения и для других операторов, потому и была придумана возможность перегрузить их. Следовательно и для оператора in нужно реализовать подобный механизм.

    Перегрузка будет выглядеть, например так.

    bool operator_in(const string& key, const unordered_map<string, string>& 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<class TIn>
    	OP_IN_LVAL<TIn> operator*(const TIn& data, const OP_IN_HELP&)
    	{
    		return OP_IN_LVAL<TIn>(data);
    	}
    

    Оператор является шаблонным, что позволяет в качестве первого аргумента принимать любой тип. Теперь нам нужно как-то получить правый объект, не потеряв при этом левый. Для этого мы реализуем класс OP_IN_LVAL который будет хранить наш левый объект.

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

    Так как сам объект будет жив пока выполняется выражение, то нет ничего страшного если мы будем хранить константную ссылку на этот объект. Теперь нам остаётся лишь реализовать внутренний оператор перемножения, который будет нам возвращать результат выполнения перегруженного оператора in, само собой он будет шаблонным.

    	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);
    		}
    	};
    

    Собственно это решение уже будет работать, но оно является ограниченным и не позволит нам писать так.

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

    Для того чтобы мы имели такую возможность, нам необходимо прокидывать возвращаемое значение перегруженного оператора. Есть две версии как это сделать, одна использует возможности с++14, друга работает в рамках с++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);
    		}
    	};
    

    Так как я в основном работаю в Visual Studio 2013 я ограничен рамками С++11 к тому же решение в рамках С++11 будет успешно работать и в С++14, потому советую выбирать его.

    Пример реализации обобщённого оператора in для 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);
    }
    

    Класс 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.

    В С++17 появилась возможность писать так.

    	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;};
    

    P.S.
    Хотелось бы узнать, интересны ли вам подобные темы, есть ли смысл писать об этом тут? И хотелось бы вам узнать как реализовать другие интересные штуки?

    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")
    

    Поделиться публикацией

    Комментарии 35

      +7
      Не знаю как другие, я прочел с наслаждением :). Просто, ясно. Спасибо! Буду рад увидеть больше подобных статей, ради них большинство и ходит сюда.
        +5
        Большой минус в плане удобства — проблемы с приоритетами таких операторов на макросах.
          +1
          Насколько я понимаю, можно просто вместо оператора умножения брать за основу любой другой бинарный оператор, приоритет которого нужно унаследовать.
          +7
          читая подобные статьи становишься фанатом C++…
          хотя, кому я вру, не становишься, а болезнь еще сильнее начинает прогрессировать
            +3
            Подброшу в тему полезную ссылку из недавнего — изящную реализацию any_of без злых макросов, но с шаблонной магией
              +2
              В С++20 должна появится возможность писать так.

              По моему эта возможность появилась в C++17
                0
                Да вы правы, спасибо, поправил.
                +15
                имхо, фо фан это круто и все такое, но увидеть подобный код в продакшене я бы не хотел …
                  +1
                  Круто! Я бы не стал делать макрос, а оставил бы вариант *in*:
                  if (auto res = "true" *in* some_map)

                  Для этого придётся сделать глобальную переменную in, зато она уже нормально включается в namespace. Понятно, что сама она состояние хранить не сможет, нужно на первом умножении создавать временное значение и хранить ключ там.
                    0

                    Да, именно так, только вместо *|, используется — так.

                    0
                    Т.е. это не обобщенное решение, а надо писать перегрузку для каждого существующего контейнера?
                      0
                      Да, но в ссылке на github можно найти готовую реализацию для некоторых контейнеров (unordered_map, unordered_set, map, set, vector, string)
                      0
                      Почему бы не использовать std::optional или boost::optional
                      он имеет каст к bool. преследует как раз safe bool idiome и нужен как раз для таких случаев

                      godbolt.org/g/Bz35nQ
                        +2
                        Конечно можно, в статье это просто пример, возвращать можно в принципе что угодно. Спасибо за совет.
                        –9
                        #define in :

                        KISS

                          0
                          В данной статье in используется не для итерации по контейнеру.
                          +5

                          Хм… Странный подход. Сейчас, вроде, в C++ наметился тренд в сторону функциональной нотации. То есть не key in map, а contains(map, key) например. Но это ладно, это мелочи. В предложенном методе нарушается подход Single Responsibility. Это как если бы в std::stack был метод pop_and_get (например), или pop не только бы извлекал элемент со стека, но и возвращал бы его. Но такого метода нет (можно у Саттера почитать, почему) — и это правильно. Так же и тут: зачем в одном операторе совмещать две разных ответственности? Оператору in оставить возвращаемое значение bool. А то, что хочется, реализуется чуть проще:


                          template<typename M, typename K>
                          std::optional<typename M::const_iterator> from(const M& m, K&& key)
                          {
                              auto p = m.find(key);
                              if (p != m.end())
                                   return p;
                          
                              return std::optional<typename M::const_iterator>();
                          }
                          
                          //...
                          
                          if (auto res = from(some_map, "cat"))
                          {
                               std::cout << res->second << std::endl;
                          }

                          Можно даже optional на значение возвращать, чтобы не только map'ы, но и списки с векторами поддержать.

                            +1
                            Отсутствие возвращаемого значения у метода pop имеет число техническую причину, и ничего общего с SRP эта причина не имеет.
                              +2

                              Не всё так просто. Сейчас эту техническую составляющую можно убрать — pop может возвращать std::optional. Но такая операция даёт разработчику (by design!) меньше гарантий, чем пара top/pop. Возникает, например, интересный вопрос: что делать, если во время создания копии объекта для возврата из pop возникло исключение? Что делать, если это случилось на этапе создания возвращаемого значения? Гарантировать, что pop всегда будет делаться через move нельзя — для объектов в стеке эта операция может быть попросту не определена. Полагаться на RVO/copy elision тоже не всегда можно.
                              В примере с in такая реализация сужает контракт. Работая с итераторами (казалось бы, удобно!) быстро утыкаешься в то, что для ассоциативных контейнеров и сиквенсов они разные. А стоит переключиться на значения — см. выше.

                                0
                                Вот это все вами перечисленное и есть та самая «техническая составляющая».
                                  0
                                  Хм. Интересная интерпретация. :) Я думал, что «техническая причина» — это невозможность (техническая) в языке вернуть заведомо невалидное значение независимо от его типа. В С++ такой возможности действительно нет. А то, что я перечислил — это исключительно вопросы контракта. Можно реализовать так, а можно иначе. Принципиальных технический препятствий в языке для любого из вариантов нет.
                                  0
                                  Гарантировать, что pop всегда будет делаться через move нельзя — для объектов в стеке эта операция может быть попросту не определена.

                                  Ну, можно было бы сделать заenable_ifленный pop по условию наличия move и его noexcept'ности.

                                    0
                                    Ну, можно и оператор индексирования для списка написать. Тут ведь вопрос — нужно ли. :)
                                +1
                                А ещё нет list::operator[], что тоже объяснено, почему. Вот только их всё равно иногда надо использовать и то, что можно было бы с приятной либой написать в мало строчек, приходится писать в 3 раза большее количество.
                                –2
                                Спасибо, прострелил себе ногу.
                                  +1
                                  Занятный пример так называемого «академического» кода — да, так можно, но нет, так не нужно.
                                    0

                                    Спасибо, но я подожду С++20, там будет bool map::contains(key) https://en.cppreference.com/w/cpp/container/map/contains, с поддержкой transparent comparator.


                                    В Boost с версии 1.67 есть нечто подобное, без дополнительных макросов: https://www.boost.org/doc/libs/1_68_0/libs/hof/doc/html/include/boost/hof/infix.html

                                      0
                                      Спасибо, но я подожду С++20, там будет bool map::contains(key)

                                      Метод std::map::count есть уже сейчас. Возвращает не bool, а int(0/1), но этого достаточно для любого практического применения. Впрочем, автора интересовало не только наличие элемента по ключу в контейнере, но и возможность (если он там есть) его редактировать без повторного поиска.

                                      с поддержкой transparent comparator.

                                      В перегрузке с transparent comparator собака зарыта. Например:
                                      std::map<QString,Value> foo;
                                      // ...
                                      return foo.contains("bar");
                                      

                                      Операторы сравнения QString и const char * определены так, что сначала из const char * создается QString, а потом сравниваются два QString'а. Итого N*log(N) лишних аллокаций, деаллокаций и преобразований кодировок в сравнении с:
                                      return foo.contains(QString("bar"));
                                      
                                        0
                                        Кто ж мешает в кучу Qt и STL?
                                          0
                                          в данном случае QString оказался попросту первым пришедшим мне на ум классом с подобными свойствами. Однако, почему бы и не смешивать? Например, до недавних пор контейнеры Qt не умели работать с noncopyable объектами, а аналогов emplace(...) у них до сих пор нет. Стандартные алгоритмы — тоже STL.
                                      +2
                                      Спасибо за интересную статью! Почерпнул идеи для создания произвольных оператров в C++.
                                        0
                                        Оператор годный, только есть одна проблема (как и с примерами в секции PS). Этот опрератор очень удобно читать и писать. Если писать я его еще могу как то сам, то читать мне как и большинству других разработчиков приходится преимущественно чужой код.

                                        Вот и проблема, как убедить своих колег в том, что это натация стоит использования? Увидев такой код на ревью, меня первым делом спросят, где тесты? Нет тестов — значит оно точно работает (не)всегда так как я этого не ожидаю.

                                        Где тесты? Где границы применимости т.е. при каких условиях такая натация это UB? Какие есть ограничения на контейнеры, на value_type, как я могу это использовать не по назначению (скорее всего случайно), и как запретить это на уровне компилятора?

                                        Когда то я тоже увлекался подобными вещами, очень полезно и развивает, а потом в какой то момент я сказал себе, хватит заниматься борьбой с ветренными мельницами (с++). Никто и никогда в здравом уме не даст мне это использовать в продакшене. А самое главное это должно быть на уровне языка, а не библиотеки (даже если это и можно с эмулировать в виде библиотеки) потому, что ни одна утилита (статический анализотр, в том числе встроенные в компилятор ворнинги) про это счастье ничего не знает и не узнает. Значит только внимательное код ревью спасет меня и моих колег от проблем, но у меня и так полно мест за которыми мне нужно очень внимательно следить, я не хочу загружать в свой мозг еще и вот это все.

                                        Еще раз, мне это очень нравиться как концепция и на уровне языка я всеми конечностями за такое, но это поделка на коленке почти наверняка не пригодна к применению в продакшене. Продакшен код нужно читать как очень плохой, несогласованный, полный багов, спагети код, который при малейшей возможности всегда проявляет UB. А еще мои (да и не только мои) колеги очень упрямые и даже если покрыть все 3х этажными тестами на все случаи жизни и предвидеть будующие измениния с++ (ну хотя бы лет на 5-6), то даже в этом случае у меня почти нет шансов убедить хотя бы парочку, что это того стоит.

                                        К сожалению практической пользы в лучшем случае 0 (а для многих вообще отрицательная), но читать было приятно, пишите есчо, есть вероятность, что другие поделки будут полезней.
                                          0
                                          Простите промахнулся ответом. (ответ ниже)
                                            +1

                                            Консерватизм свойственен людям, тут ничего не поделаешь. Можно попробовать с каких-то более предсказуемых конструкций. Не сразу втаскивать оператор "in", а начать с чего-то меньшего. Но это, конечно, зависит от коллег, есть люди которые считают, что все что не предусмотрено языком — лишнее и не имеет право на существование.


                                            P.S.
                                            Мне больше повезло с коллегами сейчас. Для одной из задач, я использовал что-то типа именованных аргументов функции и перегруженный operator | для объединения операций. Если интересно, то можно посмотреть на github.

                                            0
                                            adjachenko
                                            Вот и проблема, как убедить своих колег в том, что это натация стоит использования?

                                            Ну моя задача состояла в том чтобы, поделится с сообществом своими идеями. Убедить ваших коллег использовать это или нет, уже не моя забота. Я к идеям своих подчинённых, отношусь например без предубеждений.
                                            Где границы применимости т.е. при каких условиях такая натация это UB?

                                            Это всего лишь перегруженный оператор * UB у него такие же как и у любого другого перегруженного оператора *.
                                            Какие есть ограничения на контейнеры, на value_type, как я могу это использовать не по назначению (скорее всего случайно), и как запретить это на уровне компилятора?

                                            Это зависит уже от перегруженного оператора in, в принципе его перегрузить можно для чего угодно, я не вижу каких-то особенных препятствий.
                                            Когда то я тоже увлекался подобными вещами, очень полезно и развивает, а потом в какой то момент я сказал себе, хватит заниматься борьбой с ветренными мельницами (с++). Никто и никогда в здравом уме не даст мне это использовать в продакшене.

                                            Я бы не назвал, это борьбой с ветряными мельницами, я например делаю это для собственного развлечения в основном. Ну а если вам не разрешают использовать это в продакшене, то почему бы не использовать это в своём пэте, раз вам понравилось.
                                            . А самое главное это должно быть на уровне языка, а не библиотеки

                                            Я встречал довольно много библиотек которые активно используют макросы, за частую даже опасные.
                                            К сожалению практической пользы в лучшем случае 0 (а для многих вообще отрицательная)

                                            Сочувствую вам, сам когда-то работал в компании которая на С++11 то не хотела переходить, не то что там использовать подобные фичи.
                                            но читать было приятно, пишите есчо

                                            Спасибо, как раз намечается интересная статья.

                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                            Самое читаемое