Функциональные адаптеры С++. Ознакомление и описание принципа. Примеры использования std::bind, std::bind1st, std::bind2nd

Что такое функциональные адаптеры в С++? Это такие функциональные объекты, которые принимают на вход функцию с одним числом параметров и выводят из них функции с другим числом параметров.
Используя функциональный адаптер, можно оформить функцию с одним параметром как функцию без параметров. Внутри самого адаптера можно сохранить то значение, которое подавалось бы функции с одним параметром. Выглядит это следующим образом:

Как вы можете заметить, в конечном итоге в листинге #1 мы запускаем подобие функции без параметров, а тот параметр, который в неё уходит, уходит туда извнутри функционального объекта.
Для тех, кому это мало, о чём говорит, напишу код функционального объекта без использования bind. Только будьте готовы к тому, что понять его сложнее.

Вообще, понятие "адаптер" не имеет отношения конкретно к С++, этот термин пришёл из паттернов, реализации адаптеров могут быть очень сильно отличимы друг от друга. В нашем случае мы ограничились адаптированием функций. В листинге #2 используется перегрузка перегруженной операции (). Первый вариант перегрузки предназначен для приёма входящих значений, где первое значение указатель на функцию, а второе значение — это значение обычного числа. Оба входящих элемента сохраняются в атрибуты нашего класса, чтобы они потом могли использоваться в варианте для вызова объекта с помощью пустых круглых скобок. Таким образом мы и имеем один метод для приёма функции с одним параметром и второй метод, который, собственно, сам вызывает функцию, вставляя в него параметр, значение которого сохранено в объекте.
Раньше не было тех средств языка в С++, которые имеются сегодня, поэтому программистам периодически приходилось адаптировать функции, имеющие некоторое число параметров, к функциям с другим числом параметров. Один из классических примеров использования функциональных параметров приводится с помощью вектора из STL. Пусть, например, задан вектор и нужно каждый его элемент умножить на 5. В STL для умножения есть готовый функтор, который описан в functional: std::multiplies;

Этот функтор принимает два множителя.
И есть алгоритм для преобразования контейнеров: std::transform. Этот алгоритм, подобно многим другим, принимает четыре параметра: начало и конец последовательности, выходной параметр и функтор, определяющий непосредственную задачу ему.

И подумал какой-нибудь Серёжа-кодер: "А если у меня есть предописанное умножение, то зачем мне писать свою функцию?". Подумал и решил, что в этот раз будет использовать готовые средства. Начинает пробовать совместить алгоритм transform c функтором multiplies. При попытке написать код возникает перед Серёжей вопрос. Чтобы было понятнее, пока что используем обычную функцию:

Возникает вопрос: "А как же умножить текущее значение на 7?". Имеется в виду код из листинга #4, где непонятное обозначено вопросами.
Алгоритм std::transform написан таким образом, что в нём действительно можно использовать готовый функтор, но при этом он написан так, что ожидает себе функтор с одним параметром, при этом в старых компиляторах в качестве функтора не подойдёт название функции (только класс (структура или объединение)), а кроме того, он ещё и требует себе явного указания типов параметров. Такой вот сложный сценарий выбрали создатели STL. Нас же сейчас интересует не почему у них так получилось, и не что это за сценарий такой, а только то, что алгоритм может использовать функтор, но требует, чтобы запихиваемый функтор был с одним параметром.

Проще говоря, чтобы использовать уже готовый, описанный функтор, в алгоритме std::transform нужно переоформить функтору количество параметров, а сделать это можно с помощью функционального адаптера. Для лучшей ясности не будем использовать готовый функтор и готовый функциональный адаптер, а напишем свои. Правда, количество кода будет больше, чем могло бы быть, но иначе мне сложно показать принцип, поэтому сначала смотрим на самописное

Поскольку мы приспосабливали себе свой функциональный адаптер к требованиям алгоритма std::transform, то нам пришлось приспособиться к некоторым требованиям этого алгоритма. Поверьте, если у вас мало опыта и вы плохо понимаете шаблоны, воспроизвести эту реализацию может быть проблемно. С этим примером мне помог человек с ником Azazel-San. Я не могу углубляться и углублять вас в детали капризов. Надеюсь, что этот код поможет понять общий принцип работы встроенного в STL функционального адаптера bind и его версий: bind1st и bind2nd.
При перегрузке операции () мы описывали, что принимаем один параметр. Это потому что алгоритм std::transform требует функтор с одним параметром. Этот параметр представляет для нас сейчас второй множитель. Первый множитель пришёл вовнутрь функтора во время создания объекта, благодаря конструктору с двумя параметрами.
Если листинг #5.1 более-менее понятен, то теперь делаем всё то же самое, но с помощью уже готовых средств:

bind1st — это функциональный адаптер, который принимает функцию с двумя параметрами, запоминает в себя первый. Так получается, что первый множитель сохраняется внутри, а второй подаётся чуть позднее. Своеобразная ленивая подача второго значения. После того, как второе значение подано, он производит умножение и отдаёт нам результат. Принцип этого дела показан в листинге #5.1
Если бы мы использовали bind1st с нашей собственной структурой, то по нехватке знаний могли бы столкнуться с некоторой проблемой:

При попытке скомпилировать код из листинга #6 мы получаем неудачу. И, скорее всего, причину ошибки понять неподготовленному человеку довольно тяжело. Проблема возникает только от того, что функциональный адаптер bind1st (как и его сородич: bind2nd) написаны таким образом, что им нужно предварительно обозначить типы для некоторых предустановленных идентификаторов. Там внутри такая кухня, что назначен синоним типу, а синоним какого именно типа не указано, и эта обязанность возложения отведена на пишущих код. Поэтому нам надо задать синонимы, только если обычно задают синонимы существующим типам, то здесь надо синониму задать тип. А кроме того, функтор предполагает, что будет использован константный метод, а мы ему суём неконстантный.

Есть более короткий способ проделать то же самое:

В листинге #7.2 использован приём наследования. В binary_function уже описана подстановка типов на названия "first_argument_type", "second_argument_type" и "result_type". От нас требуется только указать эти типы в угловых скобках, поскольку структура binary_function шаблонная.
  • В С++11 bind1st и bind2nd устарели, поэтому не рекомендуются к использованию.
  • std::bind на момент написания статьи ещё не устарел, но в связи с тем, что он легко заменяется лямбда-функциями, использовать его также крайне не рекомендуется.
Если вы будете работать в команде и решите выделиться своими знаниями о std::bind, то вряд ли ваша команда оценит это, а вот воспринять за это как человека с завышенным чувством важности — легко. Поэтому не пытайтесь выделиться с помощью дохлого номера и используйте лямбда-функции вместо std::bind. В старых проектах можно встречать эти бинды, потому что язык С++ не был развит как сейчас, не обновлялся около десяти лет. И вот в таком случае в умении прочитать код с такими элементами ничего плохого нет. Реализацию std::bind не буду, и не смогу, но сам принцип работы должен бы быть уже ясен читателю. Поэтому просто покажу и опишу, как он используется.

По листингу #8 вы можете понять, что аргумент, указываемый справа в круглых скобках занимает указанную позицию. std::placeholders обозначает так называемое знакоместо, т. е. позицию аргумента из вызова, позицию, из которой должна произойти вставка. Количество знакомест зависит от количества параметров функции.

В листинге #9.2 происходит ошибка компиляции, потому что попытка выполнить вставку из второго аргумента из вызова неудачна: как мы можем вставить второй аргумент вызова, если в вызове нет второго аргумента? Чтобы второй аргумент существовал, нужно сделать вызов, подавая в вызов два и более аргументов:

В общем, std::placeholders помогает перетасовать подачу аргументов. Цифра с впереди идущим подчёркиванием буквально означает, что сюда идёт согласный цифре (числу) аргумент: _1 сюда первый аргумент вызова, _5 сюда пятый, _10 сюда десятый. В итоге порядок аргументов в самом вызове один, а в обработке может быть переиначен. Сам порядок легко определить по скобкам бинда, а в скобках вызова иногда, как в случае листинга #9.2, могут быть востребованы аргументы-заглушки. Сами заглушки использоваться не будут, но они помогают наращивать число аргументов вызова до необходимого. Так, в листинге #9.3 можно дословно читать, что сюда вставили значение второго аргумента из вызова, а после него вставили значение 6.

В общем, как уже говорилось, конечный порядок подстановки можно понять из аргументов, поданных в std::bind. И иногда могут иметь место заглушки.
Сам функциональный адаптер std::bind был добавлен в С++11 вместо объявленных устаревшими std::bind1st и std::bind2nd. Он более продвинутый и ограничен большим числом параметров. Но число параметров, точнее число знакомест ограничено, при чём само ограничение зависит от реализации поставщиков компиляторов. Т. е. не получится использовать какое-нибудь знакоместо порядка _1000000, такое число параметров никто и никогда не использует. А если использовать большое число, доступное для одного компилятора, то не факт, что другой компилятор сможет использовать столько же.
Отдаваемый тип std::bind не специфиируется, поэтому у вас не получится правильно использовать явный тип для указания функтора-сохранителя. По этой причине обязанность определения типа отводится компилятору, и мы используем auto вместо того, чтобы пробовать явно обозначить указатель.
До С++11 обходились только std::bind1st и std::bind2nd, или писали свои бинды. Уже готовые первые два из отмеченных намного проще для понимания новичками: один помогает подставлять некоторое значение на место первого аргумента, а второй на место второго. При этом они не умеют работать напрямую с именами функций, а требуют себе только структуру, класс или объединение, являющихся функторами. Об этом ранее уже было написано. Но повторим:

Подводим итоги:

  • Функциональные адаптеры — это такие функторы, которые преобразовывают функции, имеющие какое-то число параметров, в функции с другим числом параметров.
  • В С++ имеются встроенные функциональные адаптеры:

    • С++03: bind1st и bind2nd
    • С++11: bind
  • Встроенные функциональе адаптеры семейства bind использовать не рекомендуется, с С++11 предпочтение отдаётся лямбда-функциям.
  • Функциональные адаптеры можно использовать в алгоритмах STL, а также в других, невстроенных, библиотеках языка.
  • В функциональном адаптере std::bind указываются знакоместа с помощью обозначения std::placeholders. Значения аргументов с помощью этой инструкции вставляются в указанные позиции при срабатывании функции.
  • Тип возвращаемого значения std::bind не специфируется, поэтому для сохранения функтора нужно использовать приём автовыведения типа, т. е. использовать auto.
  • В С++03 во многих случаях использовались bind1st и bind2nd. Внутри них заданы три названия для типов, а типы этим названиям часто надо было задавать в ходе написания кода.
Будьте внимательны к количеству параметров и к их позициям, если доведётся использовать встроенные в STL бинды. Человеку с ником Azazel-San ещё раз спасибо, он очень помог, показал основные примеры, которые я использовал для этой статьи.
Все комментарии на сайте проверяются, поэтому ваш комментарий может появиться не сразу. Для вставки кода в комментарий используйте теги: [php]ВАШ_КОД[/php]

Добавить комментарий

Ваш e-mail не будет опубликован.

Поиск

 
     

Случайная книга в электронном формате

https://www.litres.ru/robert-s-martin/chistyy-kod-sozdanie-analiz-i-refaktoring-2/?lfrom=15589587
Яндекс.Метрика