Объединяем функции логическими операторами в C++

Original author: Jonathan Boccara
  • Translation
В преддверии старта занятий в новом потоке группы «Разработчик С++» подготовили перевод интересного материала.





Большинство алгоритмов STL в C++ используют всего лишь одну функцию для выполнения некоторой работы над коллекцией. Например, чтобы извлечь все четные числа из коллекции, мы можем написать такой код:

auto const numbers = std::vector<int>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto results = std::vector<int>{};

std::copy_if(begin(numbers), end(numbers), back_inserter(results), isMultipleOf2);


Предполагая, что у нас есть функция isMultipleOf2:

bool isMultipleOf2(int n)
{
    return (n % 2) == 0;
}

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

Но C++ не поддерживает комбинации функций. Например, если у нас также есть функция isMultipleOf3 и мы хотим извлечь числа, кратные 2 или кратные 3, было бы довольно просто написать такой код:

std::copy_if(begin(numbers), end(numbers), back_inserter(results), isMultipleOf2 || isMultipleOf3);

Но он не компилируется: в C++ не существует такой вещи как оператор || для функций.

Самый простой способ, который предлагает стандарт C++ (начиная с C++11), — использовать лямбду:

std::copy_if(begin(numbers), end(numbers), back_inserter(results), [](int number){ return isMultipleOf2(number) || isMultipleOf3(number); });

Этот код компилируется и извлекает из коллекции числа, кратные 2 или 3.

Но так код приобретает нежелательный излишек:

синтаксис лямбды: скобки [], список параметров, скобки {...} и т. д.

параметр: number.

Действительно, нам не нужно знать об отдельных параметрах, передаваемых объекту функции. Цель алгоритма — повысить уровень абстракции до уровня коллекции. Мы хотим, чтобы код выражал, что мы извлекаем такие типы чисел из коллекции, а не то, что мы делаем с отдельными числами. Даже при том, что во время выполнения результат один и тот же, это неправильный уровень абстракции в коде.

Вы можете подумать, что использование лямбд в этом случае оправдано. Но в случае, если вас раздражает дополнительный код, который они заставляют нас писать, давайте рассмотрим другие способы объединения функций с логическими операторами, такими как ||.

Я не утверждаю, что эти методы лучше, чем лямбда, все они имеют свои преимущества и недостатки. В любом случае изучение всегда поучительно. И если у вас есть какие-либо мысли по этому поводу, я хотел бы услышать их в разделе комментариев.

Решение № 1: разработка функции объединения


Я не думаю, что есть способ написать оператор || для функций в общем случае, так чтобы можно было написать isMultipleOf2 || isMultipleOf3. Действительно, функции в общем смысле включают лямбды, и лямбды могут быть любого типа. Таким образом, такой оператор будет оператором || для всех типов. Это было бы слишком навязчиво для остальной части кода.

Если мы не можем получить оператор ||, давайте разработаем функцию для его замены. Мы можем назвать ее как-нибудь похоже на «or». Мы не можем назвать ее «or», потому что это имя уже зарезервировано языком. Мы можем либо поместить ее в пространство имен, либо назвать ее как-нибудь еще.

Было бы разумно поместить такое общее имя в пространство имен, чтобы избежать коллизий. Но в целях примера давайте просто назовем ее or_. Целевое использование or_ будет следующим:

std::copy_if(begin(numbers), end(numbers), back_inserter(results), or_(isMultipleOf2, isMultipleOf3));


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





or_ — это функция, которая принимает две и возвращает одну функцию. Мы можем реализовать это, возвращая лямбду:

template<typename Function1, typename Function2>
auto or_(Function1 function1, Function2 function2)
{
    return [function1, function2](auto const& value){ return function1(value) || function2(value); };
}

Мы сделали выбор, принять параметр лямбда по const&. Это связано с тем, что в алгоритмах STL без сохранения состояния — без стресса, а это означает, что все проще, когда функциональные объекты не имеют побочных эффектов в алгоритмах STL, в частности предикатов, как у нас здесь.

Решение № 2: оператор || для определенного типа


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

Мы можем обойти это ограничение, зафиксировав тип:

template<typename Function>
struct func
{
   explicit func(Function function) : function_(function){}
   Function function_;
};

Затем мы можем определить оператор || для этого типа, и он не будет конфликтовать с другими типами в коде:

template<typename Function1, typename Function2>
auto operator||(func<Function1> function1, Function2 function2)
{
    return [function1, function2](auto const& value){ return function1.function_(value) || function2(value); };
}

Полученный код имеет преимущество в том, что || есть в его синтаксисе, но недостаток в конструкции func:

std::copy_if(begin(numbers), end(numbers), back_inserter(results), func(isMultiple(2)) || isMultiple(3));

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

Решение № 3: Использование Boost Phoenix


Цель библиотеки Boost Phoenix — написать объект сложной функции с простым кодом! Если вы не знакомы с Boost Phoenix, вы можете ознакомится с введением в Boost Phoenix, чтобы увидеть код, который она позволяет писать.

Boost Phoenix, хотя и впечатляющая библиотека, не может творить чудеса и не компилирует наш начальный целевой код (isMultipleOf2 || isMultipleOf3). Но она позволяет использовать создание объектов из isMultipleOf2 и isMultipleOf3, которые будут совместимы с остальной частью библиотеки.

Boost Phoenix вообще не использует макросы, но для этого конкретного случая:

BOOST_PHOENIX_ADAPT_FUNCTION(bool, IsMultipleOf2, isMultipleOf2, 1)
BOOST_PHOENIX_ADAPT_FUNCTION(bool, IsMultipleOf3, isMultipleOf3, 1)

Первая строка создает IsMultipleOf2 из isMultipleOf2, и мы должны указать, что isMultipleOf2 возвращает bool и принимает 1 параметр.

Затем мы можем использовать их таким образом (пример с полным кодом, чтобы показать #include):

#include <boost/phoenix/phoenix.hpp>
#include <vector>
 
bool isMultipleOf2(int n)
{
    return (n % 2) == 0;
}
 
bool isMultipleOf3(int n)
{
    return (n % 3) == 0;
}
 
BOOST_PHOENIX_ADAPT_FUNCTION(bool, IsMultipleOf2, isMultipleOf2, 1)
BOOST_PHOENIX_ADAPT_FUNCTION(bool, IsMultipleOf3, isMultipleOf3, 1)
 
int main()
{
    auto const numbers = std::vector<int>{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    auto results = std::vector<int>{};
 
    using boost::phoenix::arg_names::arg1;
    std::copy_if(begin(numbers), end(numbers), back_inserter(results), IsMultipleOf2(arg1) || IsMultipleOf3(arg1));
}

Ценой за хороший синтаксис — использование ||, это появление arg1, что означает первый аргумент, переданный этим функциям. В нашем случае объекты, успешно переданные этой функции, являются элементами внутри чисел коллекций.

Итак, что вы думаете об этих методах объединения нескольких функций логическими операциями? Видите ли вы другие способы написать это более выразительным кодом?

Бесплатный вебинар: «Контейнеры STL на все случаи жизни»
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Comments 15

    +6
    интересно не с практической но чисто академической точки зрения — грандиозное усложнение кода ради незначительного и весьма ситуациативного бонуса в лаконичности. Кто-то мог бы подумать что это и есть «c++ way», по мне так лучше просто написать лямбду.
      +1
      Как по мне: излишнее усложнение. Да, у нас будет лаконичнее код, но сколько всего будет требоваться написать или подключить для этого?
      Как по мне (я думаю и для других людей), проще будет написать ламбду и двинуться дальше, чем тратить силы на то, чтобы вот такой вот визуальный «хак» применять к коду.
        0
        А не проще написать функцию isMultipleOf2OrOf3 в одну строку?
          +1

          isMultipleOf2(x) || isMultipleOf3(x)

            0
            Ну да, именно так

            bool isMultipleOf2OrOf3(int n)
            {
                return (isMultipleOf2(x) || isMultipleOf3(x));
            }

          +2
          Пока мир идёт в сторону лаконичных стрелочных лямбд, С++ продолжает поедать кактусы, собирая в одном месте всё больше разных скобочек.
            0
            А помоему, при всех этих наворотах, нормальное решение так и не показано…
            Что мешает сделать что-то типа (не проверял, может где-то накосячил, тут только идея):
            template<class Arg>
            std::function<bool(Arg)> operator||(std::function<bool(Arg)> op1, std::function<bool(Arg)> op2)
            {
                return [op1, op2](Arg a) {return op1(a) || op2(a);}
            }
            


            И тогда можно спокойно использовать тот синтаксис что хотелось в самом начале.
              +1
              Статью Вы, по всей видимости, не читали, либо С++ для Вас не основной язык.
              Дело в том, что то, что Вы написали, применимо к объектам типа std::function ..., а передаваемые аргументы — это указатель на функцию, которые сначала надо завернуть в std::function. Чтобы приведённый оператор сработал, у Вас хотя бы один из аргументов должен уже быть std::function, что ровным счётом то, на что указал автор в примере
              std::copy_if(begin(numbers), end(numbers), back_inserter(results), func(isMultiple(2)) || isMultiple(3));
                +1
                Я бы даже ещё подробнее расписал проблему.
                Вроде бы можно написать operator|| для указателей на функции.
                Но нет. Потому что это встроенный тип.

                Код
                #include <vector>
                #include <functional>
                
                template<typename ... Args>
                using Pred = bool (*) (Args ...);
                
                template<typename ... Args>
                std::function<bool(Args ...)> operator||(Pred<Args ...> op1, Pred<Args ...> op2)
                {
                    return [op1, op2](Args ... args) {return op1(args ...) || op2(args ...);};
                }
                
                bool isMultipleOf2(int n)
                {
                    return (n % 2) == 0;
                }
                
                bool isMultipleOf3(int n)
                {
                    return (n % 3) == 0;
                }
                
                int main() {
                    auto orr = operator||(&isMultipleOf2, &isMultipleOf3);
                
                    return 0;
                }
                


                Более-менее внятную диагностику дают Clang и MSVC.

                www.godbolt.org/z/UraL8c
              0
              Хех, забавно наблюдать, как монады расползаются из функциональщины и начинают просачиваться везде!
              image
              Собственно, wiki утверждает, что монада — «это абстракция линейной цепочки связанных вычислений. Монады позволяют организовывать последовательные вычисления.». Что мы здесь и наблюдаем:
              or_(isMultipleOf2, isMultipleOf3))

              Шашкель и Шкалка для программистов на Плюсах заходят плохо (синтаксис категорически разный), может эта статья более понятно объяснит использование монад в языках, не являющихся функциональными…
                0
                Шашкель и Шкалка для программистов на Плюсах заходят плохо (синтаксис категорически разный)

                Особенно плохо заходят функции вида:


                :.
                  0

                  На плюсах такое проворачивается через дикую магию шаблонов пополам с бустовыми макросами. Писать на плюсах также как на шарпе или яве не выйдет.

                    0
                    Писать на плюсах также как на шарпе или яве не выйдет.

                    Так как на Шарпах или Жабе — не выйдет. Можно изобрести свой путь. Вообще, монада (в самом первом приближении) — функция принимающая 2 ф-ции и вызывающая их последовательно. А уж как ее оформить, дело десятое. Хоть так:
                    or_(isMultipleOf2, isMultipleOf3)

                    Хоть, завернув
                    isMultipleOf2
                    в класс обертку и реализовав у нее метод
                    or_(...)
                    и тогда писать так:
                    isMultipleOf2Wrapper.or_(isMultipleOf3)

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

                      Мой посыл в том, что любые попытки писать в ФП-стиле на плюсах всегда очень многословно и не факт далеко не факт, что от таких монад код станет чище. Цепочку вызовов, которые просто пробрасывали результат в следующую функцию нельзя скрафтить без какого-нибудь кошмара- это либо класс, который возвращает ссылку на себя, что не чисто с точки зрения функционального языка, либо funcA(funcB(funcC())). Лямбды не умеют самостоятельно захватывать контекст, pattern-matching в обиходе (в том смысле, что я смогу его использовать на работе) появится только через пару лет в лучшем случае. Типажи писать и использовать сложно и больно. std::bind сам по себе неплох, но к нему еще надо функцию написать. А там и в параметры и в return типы подавай, которые нельзя абстрагировать по-человечьи из-за бесчеловечности типажей и пока отсутствующих контрактов. Нет возможности скрафтить новые типы, чтобы не уметь складывать килобайты с метрами, не написав хотя бы пол сотни строк оберток и перегрузок операторов.
                      И в лучшем случае все эти абстракции просто поднимут время компиляции проекта, в худшем оно еще и накладных расходов накинет. Но, да, наблюдать интересно.
                      Из недавнего радует что хотя бы структурное связывание появилось в cxx17.

                  +1
                  такой оператор будет оператором || для всех типов. Это было бы слишком навязчиво для остальной части кода.

                  SFINAE + is_invocable (или просто концепт invocable в C++20) в помощь.

                  Only users with full accounts can post comments. Log in, please.