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

?v=1

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

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

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

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
auto or_(Function1 function1, Function2 function2)
{
    return [function1, function2](auto const& value){ return function1(value) || function2(value); };
}

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

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


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

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

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

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

template
auto operator||(func 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 
#include 
 
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{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    auto results = std::vector{};
 
    using boost::phoenix::arg_names::arg1;
    std::copy_if(begin(numbers), end(numbers), back_inserter(results), IsMultipleOf2(arg1) || IsMultipleOf3(arg1));
}

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

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

Бесплатный вебинар: «Контейнеры STL на все случаи жизни»

© Habrahabr.ru