Комментарии 53
С универсальной ссылкой remove_reference недостаточно. Там может быть передана константная ссылка или volatile. Тогда is_same не сработает.
Да, правильное замечание, согласен! В свое оправдание скажу, что в изначальной формулировке задачи допустимые типы аргументов у обертки строго определены.
В обобщённом коде, лучше не смотреть на "допустимые типы аргументов". Здесь воспользуйся std::remove_cvref - std::remove_cv_t<std::remove_reference_t<T>>
Мне кажется, можно было бы ещё сделать интереснее, если вместо чистого равенства типов проверять, что соответствующий вызов к do_something скомпилируется. Но тогда надо вообще все перестановки аргументов проверять.
Вы имеете в виду - с неявными преобразованиями? Так-то это можно устроить через std::is_convertible
, но тут могут быть неоднозначности. Скажем, вот такой вызов сам по себе вполне легитимен:
do_something(0, true, s);
так как 0
будет неявно преобразован в false
, а true
- в 1
.
Предлагаю упороться и сделать что-то в таком ключе:
Для каждого сочетания формального и фактического типов ввести оценку совместимости (штраф за конверсию): 0 = точное совпадение (с точностью до добавления cv), 1 = арифметическое продвижение, 2 = арифметическое преобразование (например, float в int или наоборот), 3 = пользовательские преобразования; отдельные преобразования - такие, как апкастинг ссылок и указателей (включая умные) и строку к вьюшке, - сделать дешевле, на уровне 1 или 2. Для несовместимых - штраф бесконечный.
Для всех перестановок найти суммарную оценку совместимости
Выбрать абсолютный минимум; если таковых несколько, то считать ситуацию ошибкой неоднозначного сопоставления.
А ещё можно пойти от противного.
На основе имеющейся функции породить семейство обёрток со всеми перестановками. И пусть компилятор сам потрахается, находя наилучшую, как это он делает в обычной жизни.
Как можно порождать? Думаю, придётся колдовать с наследованием. Если не хочется писать все перестановки руками
struct wrapper_0 {
static R foo(A1 a1, A2 a2, A3 a3, A4 a4) { return f(a1,a2,a3,a4); }
};
struct wrapper_1 : wrapper_1 {
using wrapper_0::foo;
static R foo(A1 a1, A2 a2, A4 a4, A3 a3) { return f(a1,a2,a4,a3); }
};
// и т.д. (где, конечно, все wrapper'ы рожаются шаблоном)
struct wrapper_23 : wrapper_22 {
using wrapper_22::foo;
static R foo(A4 a4, A3 a3, A2 a2, A1 a1) { return f(a4,a3,a2,a1); }
};
struct wrapper : wrapper_23 {
using wrapper_23::foo;
};
auto foo(auto... args) { return wrapper::foo(args...); }
Я тут не придумал пока, как именно написать рекурсивную перебиралку перестановок, просто показываю идею.
Возможно, что там будет вот такое
template<class... Args> struct wrapper {
static size_t N = sizeof...(Args);
using Start = std::index_sequence<N>;
template<class Seq> using Next = ?????; // next_permutation
template<class Seq> using IsLast = std::is_same<Next<Seq>, Start>;
template<class IntSequence> struct caller;
template<size_t... Ixs> struct caller<std::index_sequence<Ixs...> {
// для конкретной перестановки
static auto foo(auto fun, type_at<Ixs, Args...>... args) {
return fun(arg_at<Ixs>(args...)...);
}
};
template<class Seq, bool Stop = IsLast<Seq>> struct stacker;
template<class Seq> struct stacker<Seq, false> :
caller<Seq>,
stacker<Next<Seq>> {
using caller<Seq>::foo;
using stacker<Next<Seq>>::foo;
};
template<class Seq> struct stacker<Seq, true> :
caller<Seq> {};
static auto foo(auto fun, auto... args) {
return stacker<Start>::foo(fun, args...);
}
};
template<class R, class... FormalArgs>
auto make_permutable_function(R (*fun)(FormalArgs...)) {
return [fun](auto... args) {
return wrapper<FormalArgs...>::foo(args...);
}
}
Понятно, я тут тоже посрезал лишние углы, чисто обозначил подход. Надо ещё перфект форвардинг добавить, потом протащить произвольные функции / функциональные объекты, бла-бла-бла...
Я может не до конца понял условие задачи, но если вы проверяете статик ассертом, что параметров 3, то почему в шаблон нельзя просто передать три разных типа?
template<typename T1, typename T2, typename T3>
void wrapper(T1&& arg1, T2&& arg2, T3&& arg3)...
И проверяйте внутри типы... И весь этот гемор с поиском типа по паку уйдёт.
"Гемор с поиском типа по паку" мне видится более универсальным решением, потому что если разнотипных аргументов у do_something
будет не три, а, скажем, 10, то в коде поменяется только static_assert
в начале wrapper
и собственно вызов do_something
и все. Впрочем, если вы напишете свое видение решения целиком, то будет интересно взглянуть :)
Кстати, если уж на то пошло, то паки позволяют написать обобщенную обертку вроде такой:
template<typename... As, typename... Ts>
void wrapper(const std::function<void(As...)>& f, Ts&&... args)
{
static_assert(sizeof...(As) == sizeof...(args), "Invalid number of arguments");
f(get_arg_of_type<As>(std::forward<Ts>(args)...)...);
}
и оборачивать функцию с любыми аргументами:
int main()
{
std::function<void(bool, int, std::string)> f {&do_something};
wrapper(f, 1, false, std::string("s"));
wrapper(f, false, 1, std::string("s"));
wrapper(f, std::string("s"), 1, false);
}
template<typename... Ts>
void wrapper(Ts&&... args)
{
static_assert(sizeof...(args) == 3, "Wrapper takes exactly 3 arguments");
auto t = std::make_tuple(args...);
do_something(std::get<bool>(t), std::get<int>(t), std::get<std::string_view>(t));
}
Если бы типы были более сложными, пришлось бы подумать как избежать лишних копий.
Да, хороший вариант, у меня был такой в качестве одного из промежуточных, когда я вспомнил, что в C++14 в std::get
добавили извлечение из кортежа не только по номеру, но и по типу, но я его отбросил, поскольку принципиальную проблему лишних копий он не решал.
Тогда перейти на forwad_as_tuple:
template<typename... Ts>
void wrapper(Ts&&... args)
{
static_assert(sizeof...(args) == 3, "Wrapper takes exactly 3 arguments");
auto t = std::forward_as_tuple(args...);
do_something(std::get<bool&>(t), std::get<int&>(t), std::get<std::string_view&>(t));
}
будет любопытно увидеть какой-нибудь еще более эффективный вариант в комментариях.
boost parameter library?
3 аргумента это всего 6 комбинаций…
void wrapper(bool a, int b, string_view c) { do_something(a, b, c); }
void wrapper(bool a, string_view c, int b) { do_something(a, b, c); }
void wrapper(int b, bool a, string_view c) { do_something(a, b, c); }
void wrapper(int b, string_view c, bool a) { do_something(a, b, c); }
void wrapper(string_view c, bool a, int b) { do_something(a, b, c); }
void wrapper(string_view c, int b, bool a) { do_something(a, b, c); }
Ну так-то да :) А если аргументов больше? А если произвольное количество? :)
А если два аргумента будут одного типа???
То надо вставить static_assert на проверку, чтобы такого не было.
Можно добавить проверку на это, что-нибудь типа такого:
template<typename T, typename... Ts>
constexpr bool check_unique()
{
using type = typename std::decay<T>::type;
if constexpr ((std::is_same<type, Ts>::value || ...)) {
return false;
} else if constexpr (sizeof...(Ts) > 0) {
return check_unique<Ts...>();
} else {
return true;
}
}
и вызвать ее в составе static_assert
:
static_assert(check_unique<Ts...>(),
"Duplicate parameter types are not allowed");
struct Fn {
bool a; int b; string_view c;
void call() { do_something(a,b,c); }
};
Так вот чем они там занимаются, на своих С++ митапах! Так и знал, что ничего хорошего там не происходит!
Не раскрыто - зачем это может быть нужно, кто пользователь такого кода и почему он не знает порядок аргументов для функции которую вызывает, но знает адрес функции и типы аргументов.
Также сильное ограничение этого подхода, поправьте если не прав - не может быть двух аргументов одного типа, ради чего?
Также как справедливо указали в комментариях выше - подобный код трудносопрвождаем.
В обычной ситуации для передачи n параметров в произвольном порядке используют какой-то вариант map и эта фишка обычно нужна для параметризации единичных вызовов на уровне бизнес логики, или близко к ней. А не для алгоритмов, которые дергаются 100500 раз в секунду и создают узкое место.
Ну это же разминка для мозгов :)
А из практических кейсов - может быть полезно для автосгенерённого кода, например в сочетании с мета-программированием, когда всё на типы завязано.
Да это просто абстрактная задача для разминки мозга. Поэтому и тег "ненормальное программирование". В том и интерес, чтобы реализовать это как можно универсальнее и эффективнее.
void do_something(bool b, int n, std::string_view s) {
std::cout << b << n << s;
}
template<typename T1, typename T2, typename T3> void wrapper(T1 t1, T2 t2, T3 t3) {
std::tuple<T1, T2, T3> input_args(t1, t2, t3);
do_something(std::get<bool>(input_args), std::get<int>(input_args), std::get<std::string_view>(input_args));
}
int main(int argc, char* argv[]) {
using namespace std::string_view_literals;
wrapper("Hello"sv, false, 5);
return 0;
}
Теперь понятно, почему мощности современных процессоров все не хватает и не хватает. Потому что на каждый сэкономленный такт приходится по десять таких решений и, что главное, задач. Вы посчитайте, в какое количество процессорного времени выходят ваши разборы. А потом подумайте, зачем они нужны и нельзя ли выполнить ИСХОДНУЮ задачу проще, до того, как пришлось придумывать функцию с произвольным порядком аргументов. Да ещё и со странным условием, что типы разные.
В брутальном решении аргументы передаются массивом типов Variant. Каждый параметр подписывается атрибутом Name. И разбирай-не хочу.
Здесь должна быть картинка с Khaby LAME
Вы посчитайте, в какое количество процессорного времени выходят ваши разборы.
И в какое же?
Здесь небудет никаких картинок, потому что в отличии от вашего массива с Variant, все будет посчитано в compile-time.
Вот как раз из-за таких людей, которые считают нормальным передать параметры массивом Variant с атрибутом Name и не хватает мощности современных процессоров...
можно еще поюзать синтаксический сахар, не имеющий отношения к задаче, и использовать что-то типа: std::is_same_v<>
Я довёл свою идею до рабочего состояния.
Пока без perfect forwarding. (Было лень, да и зашумляется код).
https://github.com/nickolaym/cpp-fun/blob/main/single-files/arbitrary-order.cpp
#include <utility>
#include <tuple>
#include <type_traits>
#include <iostream>
#include <string>
// утилита, порождающая семейство функций
template<class Fun, class... Ts>
struct remapper {
// финальный случай: когда перестановка типов сформирована
template<class... Ls> struct leaf {
// для удобства, передаём функцию первым свободным аргументом,
// а не запоминаем в замыкании (пришлось бы копировать N! раз)
auto operator()(Fun fun, Ls... ls) const {
// пользуемся тем, что типы в наборе уникальны
return fun(std::get<Ts>(std::tuple{ls...})...);
}
};
// строим дерево наследования рекурсивно.
// LT - головной набор типов (все листья этого поддерева начинаются с LT)
// MT - набор отвергнутых типов
// RT - набор оставшихся типов - перебираем их
template<class LT, class MT, class RT> struct tree;
// all = tree<{Ts...}, {}, {}>
// tree<{Ls...}, {}, {Rs...}> = tree<{Ls...,R}, {}, {Rs... \ R}> для всех R из Rs...
// подробнее:
// tree<{Ls...}, {Ms...}, {R,Rs...}> =
// = tree<{Ls...,R}, {}, {Ms...,Rs...}> - берём первый R из Rs и спускаемся
// + tree<{Ls...}, {Ms...,R}, {Rs...}> - отклоняем первый R и идём вправо по списку
// tree<{Ls...}, {Ms...}, {R}> =
// = tree<{Ls...,R}, {}, {Ms...}> - всегда берём единственный (последний) R и спускаемся
// конец рекурсии: все типы собраны в перестановку
template<class... Ls>
struct tree<std::tuple<Ls...>, std::tuple<>, std::tuple<>>
: leaf<Ls...>
{
using leaf<Ls...>::operator();
};
// конец рекурсии по Rs: берём последний вариант и спускаемся
template<class... Ls, class... Ms, class R>
struct tree<std::tuple<Ls...>, std::tuple<Ms...>, std::tuple<R>>
: tree<std::tuple<Ls..., R>, std::tuple<>, std::tuple<Ms...>>
{
using tree<std::tuple<Ls..., R>, std::tuple<>, std::tuple<Ms...>>::operator();
};
// продолжение рекурсии по Rs
template<class... Ls, class... Ms, class R, class... Rs>
struct tree<std::tuple<Ls...>, std::tuple<Ms...>, std::tuple<R, Rs...>>
: tree<std::tuple<Ls..., R>, std::tuple<>, std::tuple<Ms..., Rs...>>
, tree<std::tuple<Ls...>, std::tuple<Ms..., R>, std::tuple<Rs...>>
{
using tree<std::tuple<Ls..., R>, std::tuple<>, std::tuple<Ms..., Rs...>>::operator();
using tree<std::tuple<Ls...>, std::tuple<Ms..., R>, std::tuple<Rs...>>::operator();
};
// все перестановки собраны в супер-наследнике
using all_permutations = tree<std::tuple<>, std::tuple<>, std::tuple<Ts...>>;
template<class... Args>
auto operator()(Fun fun, Args... args) const {
return all_permutations{}(fun, args...);
}
};
// генератор лямбды, принимающей аргументы в любом порядке
template<class R, class... Args>
auto make_arbitrary_order(R(*fun)(Args...)) {
// а вот тут уже запоминаем функцию в замыкании
return [fun](auto... args) { return remapper<decltype(fun), Args...>{}(fun, args...); };
}
// функция, которую будем испытывать
void hello(bool b, int i, float f, const char* s) {
std::cout << "hello(" << std::boolalpha << b << ", " << i << ", " << f << ", " << s << ")" << std::endl;
}
int main() {
auto world = make_arbitrary_order(hello);
world(false, 123, "test", 45.67);
// специально показываю, что можно подсовывать приводимые типы
world(123, "test", 45.67, (void*)nullptr);
world("test", 123U, (void*)nullptr, 45.67f);
}
Основная идея - в том, чтобы нагенерировать в одном скопе все возможные сигнатуры функций с перестановленными аргументами. И пусть компилятор дальше сам будет решать, какая сигнатура подходит лучше, или же обнаружит неоднозначность и выдаст ошибку.
Для этого использовал такой приём: строится множественное наследование от базовых классов, в каждой первичной базе есть по нужной перегрузке функции (оператор()), а наследники тащат все эти функции в свой скоп через using.
К сожалению, при повторяющихся типах исходной функции, а также при несовместимых / неоднозначных типах фактических аргументов сейчас выдаётся невнятная ошибка. Над читаемостью можно поработать.
Реализация для любой функции с любым количеством аргументов (*больше 1) и perfect forwarding. Я не добавлял дополнительные проверки на количество аргументов и т.д. что бы сохранить читабельность кода, при желании можно их добавить.
#include <iostream>
#include <tuple>
#include <type_traits>
#include <string_view>
void do_something(bool a, int b, std::string_view c)
{
std::cout << c << " " << b << " " << std::boolalpha << a << std::endl;
}
template<typename R, typename... Args>
struct FunctionSignature;
template<typename R, typename... Args>
struct FunctionSignature<R(*)(Args...)> {
using args_t = std::tuple<Args...>;
using result_t = R;
};
template <class T, class Tuple>
struct TupleIndex;
template <class T, class... Types>
struct TupleIndex<T, std::tuple<T, Types...>> {
static inline constexpr std::size_t value = 0;
};
template <class T, class U, class... Types>
struct TupleIndex<T, std::tuple<U, Types...>> {
static inline constexpr std::size_t value = 1 + TupleIndex<T, std::tuple<Types...>>::value;
};
template<typename SourceTuple, typename TargetTuple, std::size_t... I>
constexpr auto BindIndexes(std::index_sequence<I...>) {
return std::integer_sequence<std::size_t, TupleIndex<std::tuple_element_t<I, TargetTuple>, SourceTuple>::value...>{};
}
template<typename F, typename Tuple, std::size_t... I>
constexpr decltype(auto) Apply(F&& f, Tuple&& tuple, std::index_sequence<I...>) {
return std::invoke(std::forward<F>(f), std::get<I>(std::forward<Tuple>(tuple))...);
}
template<typename F, typename... T>
constexpr decltype(auto) InvokeRandomArgs(F&& f, T&&... args) {
using SourceArgs = std::tuple<T...>;
using SourceSequence = std::index_sequence_for<T...>;
using TargetArgs = typename FunctionSignature<F>::args_t;
return Apply(std::forward<F>(f), std::forward_as_tuple(std::forward<T>(args)...), BindIndexes<SourceArgs, TargetArgs>(SourceSequence{}));
}
int main()
{
InvokeRandomArgs(&do_something, std::string_view{ "Hello" }, true, 20);
InvokeRandomArgs(&do_something, false, 40, std::string_view{ "World" });
InvokeRandomArgs(&do_something, 14, std::string_view{ "C++" }, true);
InvokeRandomArgs(&do_something, 17, true, std::string_view{ "std" });
return 0;
}
Ваши тесты не репрезентативны и код ломается при попытке передать не оптимизируемые аргументы в рантайме.
Как я уже написал выше, я не добавлял каких то проверок и не тестировал на всевозможных аргументах, цель этого кода показать принцип решения где используя tuple и index sequence мы простым перебором находим нужные индексы аргументов и затем передаем их в apply. Что касается ошибки в приведенных вами тестах, фикситься одной строчкой. using SourceArgs = std::tuple<std::remove_reference_t<T>...>;
https://godbolt.org/z/YfK11hG5G
Не исключаю, что могут быть и другие ошибки если использовать всевозможные ссылочные типы, const, volatile в качестве аргументов, но опять же это все можно пофиксить.
Я давно не C++-ник, но интересно:
> Остается одно лишнее копирование/перемещение - ведь, возвращая объект типа R
, компилятор вынужден его создать, а RVO здесь не сработает.
Можно ли как-то обнаружить неработающий RVO кроме как умозрительно или разбираясь на уровне asm?
> (std::string_view
там заменен на более "тяжелый" std::string
для более наглядной демонстрации работы perfect forwarding).
Где можно посмотреть эту наглядность (на godbolt?)?
Можно ли как-то обнаружить неработающий RVO кроме как умозрительно или разбираясь на уровне asm?
RVO работает только если объект создается в функции, которая его возвращает (причем с рядом ограничений). Если объект пришел в функцию так или иначе извне, то никаких оптимизаций не происходит. Вот здесь написано подробнее.
Где можно посмотреть эту наглядность (на godbolt?)?
Косвенно это видно по тому, что остается внутри s0
после передачи ее в обертку "как есть" и через std::move
в различных вариантах последнего аргумента у do_something
. Скажем, если do_something
принимает std::string
, то при передаче "как есть" произойдет копирование s0
в c
(и s0
останется неизменной), а при передаче через std::move
произойдет перемещение s0
в c
(и s0
превратится в пустую строку, фактически "обменявшись" своим состоянием с default constructed c
). Если же do_something
принимает const std::string &
, то ни копирования, ни перемещения не произойдет вовсе, и s0
останется неизменной в обоих случаях. А для еще большей наглядности конечно лучше передавать свой класс с отладочной печатью в соответствующих конструкторах.
Как то сложно ц вас всё получается
template <class... Ts>
void wraper(Ts... vals)
{
static_assert(sizeof...(Ts) == 3);
auto tuple = std::tuple { vals... };
do_something(std::get<bool>(tuple), std::get<int>(tuple), std::get<std::string_view>(tuple));
}
У меня даже на O1 всю шаблонную дичь выкинуло.
Этот вариант плох тем, что появляются совершенно лишние операции копирования. См. обсуждение аналогичного варианта от encyclopedist здесь же в комментариях.
P.S. Да, еще окончательный вариант из статьи легким движением руки трансформируется для оборачивания функций с любым количеством аргументов различного типа, а предлагаемый вами вариант практически не масштабируется.
Про копирование что то не подумал. А стоит ли того такое переусложнение кода ради попытки убрать пару тройку mov`ов?
Для случая с bool, int и string_view — нет, переусложнение кода того не стоит.
А вот если считать что эти типы тут только для примера приведены и решать задачу в общем случае — то надо помнить, что копирование может оказаться очень дорогой операцией.
Переработал код в примерах выше и теперь всё без копирования
template <class... As, class... Ts>
void wraper(void (*f)(As...), const Ts&... vals)
{
static_assert(sizeof...(Ts) == 3);
auto&& tuple = std::tuple<const Ts&...> { vals... };
f(std::get<const std::decay_t<As>&>(tuple)...);
}
Опять-таки нет perfect forwarding, соответственно, будет копирование там, где могло бы быть гораздо более дешевое перемещение:
Что и куда форвардить если всё по ссылке летит?
Задачка о функции-обертке, принимающей аргументы в произвольном порядке, и ее решение на C++17