Не буду сильно углубляться в теорию. Что такое частичное применение легко найти в интернете. В том числе на Википедии.
Если кратко, то это механизм, позволяющий зафиксировать k
аргументов функции от n
аргументов, сделав из неё функцию от (n - k)
аргументов.
// Пусть имеется функция f от четырёх аргументов:
int f (int a, int b, int c, int d)
{
return a + b + c + d;
}
// Фиксируем первые два аргумента:
auto g = part(f, 1, 2); // 1 + 2 + ...
// Добрасываем оставшиеся два:
assert(g(3, 4) == 10); // ... + 3 + 4 = 10
На эту тему уже существует масса публикаций, в том числе и на Хабре:
- C++ Variadic templates. Каррирование и частичное применение
- Частичное применение и каррирование в C++
- Каррируем на C++
А ветка "How should I make function curry?" на stackoverflow — просто кладезь для тех, кто впервые сталкивается с этой темой.
К сожалению, количество пока не переросло в качество, и хорошего, пригодного к использованию варианта я так и не увидел. При этом любопытно вот что.
Замечательный факт №1. В упомянутых статьях присутствуют все техники, которые нужны для реализации правильного (по моему мнению) частичного применения.
Надо только всё внимательно проанализировать и сложить кубики в правильном порядке. Именно этим я и собираюсь заняться в данной статье.
Содержание
Цели
Итак, какие цели стоят перед нами:
Собственно реализация требуемой функциональности
То есть реализация в каком-то виде частичного применения функции.
Максимально возможная эффективность
Нельзя забывать, что мы пишем на языке C++, одним из основных принципов которого является принцип "Абстракции без накладных расходов" (см. Stroustrup, Foundations of C++).
В частности, не должно произойти ни одного лишнего копирования и ни одного лишнего переноса.
Скрытый текстДобавлю, что это не только одно из самых важных мест в программировании на плюсах, но, одновременно, и одно из самых интересных.
Удобство в использовании
Прикладному программисту должно быть легко создавать частично применённые функции.
Также ему должно быть легко использовать частично применённые функции совместно с уже существующими решениями. Например, передавать частично применённую функцию в какой-то стандартный алгоритм.
Существующие решения
Авторы имеющихся решений предлагают два основных варианта. Назовём их применение по возможности и явное применение.
Применение по возможности
Эта техника заключается в том, что результат частичного применения — это функция, которую снова можно частично применить. А вызов исходной функции происходит тогда, когда полученных аргументов уже достаточно для того, чтобы произвести вызов.
// Пусть дана функция:
int f (int a, int b, int c, int d);
// Частичное применение функции:
auto g = part(f);
// Фиксируем первый элемент:
auto h = g(1);
// Одного аргумента недостаточно для вызова функции `f`, поэтому функция пока не вызывается.
// Забрасываем ещё два аргумента:
auto i = h(2, 3);
// Трёх аргументов по-прежнему недостаточно для вызова функции `f`.
// Забросили последний аргумент, функция может быть вызвана, следовательно,
// этот вызов и происходит.
assert(i(4) == 10);
Достаточно остроумная идея. Но, к сожалению, на практике не работает.
Почему? Допустим, мы хотим частично применить функцию, вычисляющую сумму произвольного количества переменных:
auto sum (auto ... xs);
В таком случае, мы не знаем, когда нужно остановиться в процессе частичного применения.
Эта сумма вычисляется и от одного, и от двух, и от трёх, да и вообще от любого количества переменных.
Следовательно, предыдущий пример ломается на втором шаге, если вместо f
подставить sum
:
auto g = part(sum);
auto ??? = g(1); // Уже вычислять, или пока нет?
То же самое произойдёт, если у функции есть несколько перегрузок с разным количеством аргументов. Будет непонятно, какую из них нужно вызвать.
Есть и другая проблема, когда можно "проскочить" правильное количество аргументов. В нашем примере можно изобразить так:
auto g = part(f, 1, 2, 3);
auto h = g(4, 5, 6); // Ой...
Тогда функция будет частично применяться до бесконечности, и никогда не будет вызвана.
Явное применение
Итак, — говорят авторы, — раз мы не знаем, когда нужно остановить частичное применение, давайте сделаем это явно.
А именно, создадим перегрузку оператора "скобочки" без аргументов, вызов которого будет означать, что нужно вызвать внутреннюю функцию с теми параметрами, которые уже были частично применены:
auto g = part(sum);
auto h = g(1); // Уже вычислять, или пока нет? Пока нет!
auto i = h(2, 3, 4, 5, 6); // Записываем новые аргументы и снова ждём.
assert(i() == 21); // А вот теперь вычисляем.
Старая проблема решена. Но возникла новая.
Теперь каждое использование частично применённой функции подразумевает знание о том, что это именно частично применённая функция. Это значит, что её не получится использовать в стандартных алгоритмах. Они просто не знают о том, что после забрасывания аргументов нужно ещё позвать пустые "скобочки".
Философствования
Чтобы продвинуться дальше, немного пофилософствуем и выскажем пару важных мыслей.
Мысль первая
Частичное применение является отложенным вызовом. Иначе говоря, частичное применение не производит никаких "частичных вычислений". Оно просто запоминает несколько аргументов и ждёт, пока придут остальные.
Если для суммы, произведения и других ассоциативных функций ещё можно придумать костыль, который будет уметь и возвращать частично вычисленный результат (сумму или произведение полученных ранее чисел), и продолжать частичное применение, то для произвольной функции такое решение не подойдёт.
Мысль вторая
В языке C++ нет встроенного механизма частичного применения. Это значит, что программист, вообще говоря, не ожидает, что может какой-то произвольной функции передать только часть аргументов, а остальные "добросить" когда-нибудь потом.
Следовательно, работая с частичным применением, программист заведомо знает, что он его использует.
Значит, частичное применение в языке C++ всегда будет явным.
Результат размышления
Выходит, перечисленные варианты не годятся. Из-за, казалось бы, маленьких недочётов эти "прикольные" решения приходится признавать крайне непрактичными, и потому непригодными к повседневному использованию.
Всё же не стоит забывать, что C++ не является функциональным языком. Нельзя так просто взять и перенести конструкции и идеологию одного языка на другой. Тут как с переводом поэзии с иностранного языка: перевод должен быть не дословным, а поэтическим, то есть рифмоваться в языке перевода.
Новое решение
Исходя из всего сказанного выше, я пришёл к следующему выводу. Поскольку программист всегда знает, где у него частичное применение, а где вызов функции, то можно нашу модель одновременно и упростить, и сделать более универсальной.
Состоять она будет из двух этапов:
- Захват первоначальных аргументов.
- Безусловный вызов с оставшимися аргументами.
Проиллюстрирую на примере.
// Дана функция суммирования:
auto sum (auto ... xs);
// Частичное применение суммирования к трём аргументам.
auto f = part(sum, 1, 2, 3);
// Результат сохранён в функциональном объекте `f`.
// Вызов функции суммирования:
assert(f(4) == 10);
assert(f(4, 5) == 15);
assert(f(4, 5, 6) == 21);
Если нужно частично применить ещё несколько аргументов, то они "добрасываются" при помощи того же явного вызова функции частичного применения:
auto g = part(f, 4, 5); // Эквивалентно part(sum, 1, 2, 3, 4, 5).
Это же std::bind
!
Похоже, но нет. С одной стороны, std::bind
позволяет обходиться с аргументами более гибко. Ставить их в произвольные места, перемешивать и т.п.
С другой стороны, std::bind
требует явного расставления заполнителей и не работает с произвольным числом аргументов. То есть пользователь обязан заранее указать, сколько аргументов будет "доброшено" в будущем, и на каких конкретно местах в вызове они будут стоять.
Поэтому я считаю, что получившееся решение достаточно самостоятельно и не является частным случаем каких-либо других имеющихся механизмов.
Реализация
Возможно, самая интересная часть. Код. В начале статьи я уже упоминал один замечательный факт. Так вот, есть ещё один.
Замечательный факт №2. В стандартной библиотеке (C++17) есть почти всё необходимое для реализации частичного применения по данной модели.
Нам придётся только доопределить одну операцию, которая, впрочем, тоже выражается через те же самые стандартные инструменты.
Итак, нам понадобятся (полный и исчерпывающий список):
Когда я говорил, что имеется почти всё необходимое, я имел в виду, что нужно доопределить одну функцию:
template <typename T>
constexpr decltype(auto) forward_tuple (T && t)
{
return apply(forward_as_tuple, std::forward<T>(t));
}
Данная функция принимает произвольный кортеж и возвращает новый кортеж, состоящий из ссылок на элементы входного кортежа. Если во входном кортеже лежали ссылки на lvalue
, то они такими и остаются. Те же объекты, которые хранились в кортеже, передаются по ссылке на rvalue
.
Теперь можно с уверенностью говорить, что всё необходимое у нас уже имеется. Можно писать код.
Функция
part
template <typename ... As> constexpr auto part (As && ... as) -> part_fn<decltype(std::make_tuple(std::forward<As>(as)...))> { return {std::make_tuple(std::forward<As>(as)...)}; }
Принимает произвольное количество аргументов, первым из которых является некоторая функция или функциональный объект. Сохраняет их все в один кортеж при помощи функции
make_tuple
и возвращает этот кортеж, завёрнутый в структуруpart_fn
.
Структура
part_fn
template <typename Tuple> struct part_fn { template <typename ... As> constexpr decltype(auto) operator () (As && ... as) const & { return apply(invoke, std::tuple_cat(forward_tuple(t), std::forward_as_tuple(std::forward<As>(as)...))); } template <typename ... As> constexpr decltype(auto) operator () (As && ... as) & { return apply(invoke, std::tuple_cat(forward_tuple(t), std::forward_as_tuple(std::forward<As>(as)...))); } template <typename ... As> constexpr decltype(auto) operator () (As && ... as) && { return apply(invoke, std::tuple_cat(forward_tuple(std::move(t)), std::forward_as_tuple(std::forward<As>(as)...))); } Tuple t; };
Хранит частично применённые объекты: функцию и первые её
k
аргументов.
Имеет оператор "скобочки", который принимает произвольное количество аргументов
- При вызове формируется кортеж ссылок на входные аргументы при помощи функции
forward_as_tuple
. - Кортеж с сохранёнными ранее объектами также преобразуется в кортеж ссылок при помощи определённой нами функции
forward_tuple
. - Оба кортежа ссылок склеиваются в один при помощи функции
tuple_cat
. Получается один большой кортеж ссылок. - Склеенный кортеж разворачивается и передаётся в функцию
invoke
при помощи функцииapply
. - Вызов функции
invoke
от полученных аргументов.
- При вызове формируется кортеж ссылок на входные аргументы при помощи функции
Всё.
В этом месте любители хитровывернутого шаблонного кода (как я, например), должны испытать лёгкое разочарование.
С другой стороны, простота, элегантность решения и то, что оно собрано всего из нескольких стандартных "кубиков", косвенно свидетельствует о том, что оно вполне жизнеспособно и может быть легко интегрировано в прикладной код.
Использование
int modulo_less (int modulo, int a, int b)
{
return (a % modulo) < (b % modulo);
}
auto v = std::vector<int>{...};
std::sort(v.begin(), v.end(), part(modulo_less, 7));
Благодаря вызову make_tuple
поддерживаются std::ref/cref
:
int append (std::string & s, int x)
{
return s += std::to_string(x);
}
auto s = std::string("qwerty");
auto g = part(append, std::ref(s));
g(5);
g(6);
assert(s == "qwerty56");
А благодаря использованию функции invoke
будут поддерживаться различные сложные случаи, такие как вызов метода класса (см. std::invoke) и т.п.
И всё это "из коробки" и без каких-либо специальных телодвижений с нашей стороны.
Заключение
Почему я считаю своё решение правильным.
- Оно просто и прозрачно. Собирается из "элементарных" компонент, которые уже есть в языке.
- Оно эффективно. Абстракция без накладных расходов.
- Надёжно как молоток. Никакой "магии".
- Хорошо встраивается в существующую парадигму программирования на языке C++.
- Совместимо с инструментами, которые используются в стандартной библиотеке. Например, с
reference_wrapper
для сигнализирования о передаче параметров по ссылке (как вstd::make_tuple
,std::bind
,std::thread
), а также, что очень важно, с алгоритмами.
» Полный исходный код здесь.
Комментарии (40)
Eivind
24.10.2016 16:06+1Не проще ли сделать так?
template <class F, class... Args> auto part( F f, Args... args ) { return [=]( auto&&... args2 ) { return f( args..., std::forward<decltype( args2 )>( args2 )... ); }; }
izvolov
24.10.2016 17:00+1Понимаете ли вы, что этот код не эквивалентен моему? И почему?
Eivind
24.10.2016 17:22Например:
struct functor { void operator()( int ) const { std::cout << "int\n"; } void operator()( std::reference_wrapper<int> ) const { std::cout << "std::reference_wrapper<int>\n"; } }; int k = 10; part( functor(), k )(); part( functor(), std::ref( k ) )();
izvolov
24.10.2016 17:27Поясните, что вы имели в виду, я не понял.
Давайте я тоже переформулирую:
Я утверждаю, что ваш код не эквивалентен моему, поэтому предлагать его на замену, как минимум, некорректно.
izvolov
24.10.2016 17:32О, давайте так.
У меня есть список из пары тестиков: https://github.com/izvolov/burst/blob/master/test/functional/part.cpp
Напишите ваш код на лямбдах так, чтобы он проходил все тесты.
PkXwmpgN
25.10.2016 01:56С лямбдами будет скорее всего все как у вас, просто кортеж переедет в capture.
Типа токого
template<typename ...As> constexpr decltype(auto) part(As && ...as) { return [as = std::make_tuple(std::forward<As>(as)...)](auto && ...as2) { return apply(invoke, std::tuple_cat(forward_tuple(as), std::forward_as_tuple(as2...))); }; }
или такого
template<typename F, typename ...As> constexpr decltype(auto) part(F && f, As && ...as) { return [f = std::forward<F>(f), a = std::make_tuple(std::forward<As>(as)...)](auto&&... as2) { return apply(f, std::tuple_cat(forward_tuple(a), std::forward_as_tuple(as2...))); }; }
У вас вроде понагляднее.
Кстате, следует упомянуть что
forward_as_tuple
иinvoke
у вас — это дополнительные функциональные объекты, обертки над стандартными функциями. Стандартную функцию в apply таким образом не запихать.izvolov
25.10.2016 09:47К сожалению, так не получится.
Да, с копированием и переносом проблема решена, но с лямбдами не получится учесть различия в вызове междуlvalue
иrvalue
. У меня для этого и создан функциональный объект с отдельными операторами "()".
А различать их нужно для того, чтобы не копировать лишний раз захваченные аргументы при вызове, если известно, что вызывающий объект —
rvalue
. В этом случае захваченные аргументы "выбрасываются" наружу.PkXwmpgN
25.10.2016 11:20А различать их нужно для того, чтобы не копировать лишний раз захваченные аргументы при вызове, если известно, что вызывающий объект — rvalue
Так вроде дополнительного копирования и не будет, они же вроде по ссылкам уйдут.
auto c = Caller(); // только одно копирование, при создание объекта part, при создание кортежа. // при вызове долнительного копирование не будет part(c)(3.14);
izvolov
25.10.2016 11:23Допустим, класс
Caller
определён так:
struct Caller { auto operator () (std::string s) const { return s; } };
Тогда в следующем коде произойдёт копирование строки в момент вызова:
part(Caller{}, std::string("qwerty"))();
PkXwmpgN
25.10.2016 12:24Соглашусь, лямбдами это не покрыть. Интересно, part мне начинает нравится.
Eivind
25.10.2016 12:49Мне тоже, но я еще не придумал как его можно использовать.
Забавно, что схожее предложение на днях было опубликовано в рамках WG21.
С другой стороны данная статья может быть использована как наглядный пример для дальнейшего устранения недостатков лямбд (variadic move-capture, const mutable lambda call, rvalue lambda и т.д.)
Shamov
24.10.2016 16:25Как-то не слишком много внимания уделено целесообразности. Мне, например, не очевидно, что существует реальный кейс, в котором недостаточно std::bind. Типа, у меня уже есть функция, у которой я хочу зафиксировать несколько аргументов, но я почему-то ещё не знаю точно, сколько у неё этих аргументов вообще. Не представляю себя в подобной ситуации. Обычно, когда у меня есть конкретная функция, то я уже знаю о ней вообще всё. Уж количество аргументов так точно.
izvolov
24.10.2016 17:25+1Можно развить мой пример из статьи про сравнение по модулю
n
.
Только теперь у нас будет не сравнение по модулю, а произведение по модулю. Первым аргументом оно так же будет принимать, собственно, модуль, а остальными — значения, которые нужно перемножить.
auto modulo_product (auto modulo, auto ... as); auto f = part(modulo_product, 7); // Произведение по модулю 7. auto g = part(modulo_product, 13); // Произведение по модулю 13.
С
стд::биндом
такое не изобразишь.Shamov
24.10.2016 18:37Действительно. Через std::bind так сделать не получится. Признаю, что это реальный кейс, имеющий смысл. Хотя, справедливости ради, нужно сказать, что желание делать такое — это симптом проблемы под названием «функциональное программирование головного мозга». В плюсах такое выглядит чересчур инородно. Даже при том, что это мультипарадигмальный язык.
izvolov
24.10.2016 20:54… желание делать такое — это симптом проблемы под названием «функциональное программирование головного мозга». В плюсах такое выглядит чересчур инородно.
Обоснуете?
Shamov
24.10.2016 21:40Под мультипарадигмальностью языка я понимаю то, что он позволяет разумно сочетать различные парадигмы. Не нужно писать на плюсах так, как будто это функциональный язык. Чрезмерный перекос в сторону функциональной парадигмы точно так же неуместен, как и полный глухой отказ от всех функциональных штучек.
izvolov
24.10.2016 21:42Вы ведь статью не читали, да?
Shamov
24.10.2016 22:46Читал. Но более двух часов назад. Поэтому уже всё забыл.
izvolov
24.10.2016 22:54+1Дело в том, что у меня в статье есть следующая фраза:
Всё же не стоит забывать, что C++ не является функциональным языком. Нельзя так просто взять и перенести конструкции и идеологию одного языка на другой.
Именно поэтому я и создаю инструмент, который, по моему мнению, прекрасно вливается в идеологию плюсов. Не создаёт никаких перекосов и сильно упрощает жизнь в определённых ситуациях.
Shamov
25.10.2016 00:16Тогда нельзя засчитать пример с произведением по модулю. Это чисто функциональная мулька. В нефункциональных языках не принято делать функции, которые не имеют вообще никакого собственного содержания. Нужно фиксировать либо алгоритм, либо параметры. Структурное программирование называется структурным, именно потому что базовые блоки, из которых составляется программа, имеют определённую структуру. Они не являются полностью абстрактными операциями, приобретающими какой-либо смысл только в контексте использования. Хорошую функцию в нефункциоальном языке нельзя использовать где угодно, как угодно и для чего угодно. Обычно её можно использовать только так, как это было задумано автором. И он, естественно, не должен задумывать такое использование, которое ничем не ограничено. Если он задумывает чисто абстрактную функцию, которая может делать что угодно в зависимости от параметров, то он просто махровый функциональщик. Такой автор должен перестать обманывать себя, совершить каминг-аут и прекратить изображать из себя обычного программиста.
izvolov
25.10.2016 09:51Это чисто функциональная мулька.
В нефункциональных языках не принято…
… в нефункциоальном языке нельзя…
… не должен задумывать ...Пожалуйста, обоснуйте.
Shamov
25.10.2016 10:33Всмысле? Как ещё я могу это обосновать? Что ещё тут можно добавить? Неужели для вас является новостью то, что у функциональных языков есть некоторые дистинктивные особенности, которые отличают их от других и делают их функциональными? По большому счёту таких особенностей две. Первая состоит в том, что абстрактность функций порой опасно приближается к границе с абсурдом. Чтобы получить нормальную полноценную функцию, которая делает что-то конкретное, часто нужно соединить вместе пяток абстрактных недофункций, каждая из которых привносит в это действие какой-то отдельный аспект. Вторая особенность состоит в упоре на иммутабилити и избегании побочных эффектов. Обе эти особенности являются дистинктивными. Нефункциональным языкам они не свойственны. Про программиста, который упорно старается применить их в нефункциональном языке везде, где только может, говорят, что у него ФПГМ. Хотя умеренное применение лямбд и std::bind'а — это современная норма.
izvolov
25.10.2016 11:08+3Всмысле? Как ещё я могу это обосновать? Что ещё тут можно добавить?
Ну пока что обоснований не было. Были только эмоции.
Правильно ли я понял, что использовать лямбды и
std::bind
можно, а частичное применение в том виде, в котором я предлагаю, — нет?
Ну то есть если я напишу
const auto f = [modulo] (auto ... as) { return modulo_product(modulo, as...); };
то всё хорошо.
Но как только я написал
const auto f = part(modulo_product, modulo);
то я уже богомерзкий функциональщик, покушающийся на основы мироздания?
Shamov
25.10.2016 12:09-1Именно так. В первом случае вы создаёте лямбду в локальном контексте. Она как-то там используется. Желательно однократно. И затем быстро уничтожается. Так сказать, быстро использованная и сразу же разрушенная лямбда не считается созданной. Во втором же случае вы оформляете процесс создания таких функций в отдельную функцию, придавая тем самым этому процессу более высокий статус. Сделав такое, вы больше не можете говорить, что вы просто балуетесь функциональными штучками в мультипарадигменном языке. Потому что такая манера совершенно точно является функциональным стилем.
Это как если вы перекусываете гамбургером по пути куда-то в целях экономии времени, то это одно. Это не маркирует вас каким-то особым отношением к гамбургерам. Всякое бывает в дороге. Захотелось есть, а времени было только на гамбургер. Все всё понимают. И совсем другое дело, когда вы арендуете зал для торжественных приёмов, организуете статусное мероприятие, но при этом подаёте гостям гамбургеры. Когда вы делаете такое, вы не можете оправдаться тем, что просто сделали всё на скорую руку, и что просто не хватило времени на приготовление настоящей еды. Вас обязательно обвинят в том, что вы форсите применение гамбургеров в неуместном для этого контексте.izvolov
25.10.2016 12:15+3Дружище, извини, но это какая-то ахинея.
Shamov
25.10.2016 13:13-1Ничего страшного. Это стандартное развитие старого как мир сюжета. То, что мои рассуждения в конце концов будут квалифицированы как чушь, было понятно ещё в самый первый момент, когда в ответ на моё оценочное суждение были затребованы обоснования. Это как рак. Если бы люди не умирали от других причин, то в конце концов они умирали бы от рака. Так же и здесь. Всякий раз, если не происходит ничего по-настоящему необычного, любому собеседнику рано или поздно удаётся полностью разобраться в ситуации и понять, что я несу чушь.
Sirikid
25.10.2016 11:10+3То что в (чистых) функциональных языках нельзя* писать функции с побочным эффектом не делает использование чистых функций атрибутом только функциональных языков.
* конечно же можно.
Temtaime
25.10.2016 01:05-1А можно просто писать на D, а не усложнять себе жизнь.
alias f = partial!(modulo_product, 7);izvolov
25.10.2016 09:52+2И чем же это проще того, что сделано у меня?
Temtaime
25.10.2016 11:37-1Тем, что это уже сделано за вас.
izvolov
25.10.2016 11:40+3Начнём с того, что
partial
в языке D не поддерживает произвольное количество аргументов. Можно зафиксировать только один.
Следовательно, утверждение "это уже сделано за вас" ложно.
К тому же непонятно, что мне делать с тем, чего в языке D нет. Снова искать другой язык? Как выдумаете, удастся ли найти язык, в котором есть всё?
Temtaime
25.10.2016 15:21Вы можете объявить свою функцию и передавать сколько угодно аргументов как хотите.
auto f(A...)(A args) { return foo(1, 2, 3, args, 4, 5); }
Sirikid
24.10.2016 21:41+2А мне кажется получилось очень хорошо, я не пишу на плюсах, просто любопытствующий, тем не менее я понял как работает этот код.
Gorthauer87
24.10.2016 22:51+1Я в этом случае просто создавал лямбду. Это не очень чисто идеологически, зато дёшево и практично.
izvolov
24.10.2016 23:01+2Лямбды — это хорошо.
Но на практике выясняется, что лямбды очень часто повторяются. То есть приходится в разных участках кода в пределах одного проекта определять одинаковые лямбды. Пусть даже относительно простые.
К тому же у лямбд очень громоздкий синтаксис. Поэтому если в определённых ситуациях удаётся "собрать" функцию из "кубиков" вместо того, чтобы определять её заново, то это очень приятно.
Вотstd::bind
и моё частичное применение в этом помогают.
PkXwmpgN
Почему нельзя использовать "стандартную" сигнатуру?
Это позволит проконтролировать, что первым аргументом должен идти объект, над которым будет выполненно преобразование. А если сохранить объект отдельно от кортежа, то можно вызывать просто apply. Стандартные apply и invoke оба обрабатывают callable-объекты, и скорее всего apply будет реализован через invoke.
izvolov
Проконтролировать в каком смысле? Проверить соответствие концепту?
PkXwmpgN
Да, можно будет проверить. Ну и по аргументам видно что передавать, но это видимо ближе к плюсам, если отталкивать от функционального программирования то ваш вариант предпочтительнее.
izvolov
Лично мне непонятно, как это проверять.
В аргументах может прийти и функция, и функциональный объект, и лямбда, и
reference_wrapper
, и указатель на поле класса, и указатель на метод класса.А можно ли произвести вызов определяет
std::invoke
, но не в момент захвата аргументов, а в момент "добрасывания" оставшихся.Вот для понятности кода действительно можно было бы выделить первый аргумент. Ну и для избежания лишнего вызова
std::invoke
тоже. Просто мне не хотелось писать метафункциюunwrap_reference
, потому чтоstd::make_tuple
и так делает всё необходимое.В целом замечание справедливое, буду думать.