Использование SFINAE с общими лямбдами

Могут ли общие лямбды воспользоваться «Ошибка замещения не является ошибкой» править? пример

auto gL =
[](auto&& func, auto&& param1, auto&&... params)
-> enable_if_t< is_integral<
std::decay_t<decltype(param1)>
>::value>
{
// ...
};

auto gL =
[](auto&& func, auto&& param1, auto&&... params)
-> enable_if_t< !is_integral<
std::decay_t<decltype(param1)>
>::value>
{
// ...
};

Есть ли обходные пути или планы по включению этого в язык? Кроме того, поскольку общие лямбды являются шаблонными объектами функций под капотом, не странно ли, что это невозможно сделать?

4

Решение

Лямбды — это функциональные объекты под капотом. Общие лямбды — это функциональные объекты с шаблоном operator()s.

template<class...Fs>
struct funcs_t{};

template<class F0, class...Fs>
struct funcs_t<F0, Fs...>: F0, funcs_t<Fs...> {
funcs_t(F0 f0, Fs... fs):
F0(std::move(f0)),
funcs_t<Fs...>(std::move(fs)...)
{}
using F0::operator();
using funcs_t<Fs...>::operator();
};
template<class F>
struct funcs_t<F>:F {
funcs_t(F f):F(std::move(f)){};
using F::operator();
};
template<class...Fs>
funcs_t< std::decay_t<Fs>... > funcs(Fs&&...fs) {
return {std::forward<Fs>(fs)...};
}

auto f_all = funcs( f1, f2 ) генерирует объект, который является перегрузкой обоих f1 а также f2,

auto g_integral =
[](auto&& func, auto&& param1, auto&&... params)
-> std::enable_if_t< std::is_integral<
std::decay_t<decltype(param1)>
>{}>
{
// ...
};

auto g_not_integral =
[](auto&& func, auto&& param1, auto&&... params)
-> std::enable_if_t< !std::is_integral<
std::decay_t<decltype(param1)>
>{}>
{
// ...
};

auto gL = funcs( g_not_integral, g_integral );

и звонит gL будет делать SFINAE дружественное разрешение перегрузки на двух лямбдах.

Вышеупомянутое делает некоторые ложные ходы, которых можно было бы избежать, в линейном наследовании funcs_t, В промышленной библиотеке качества я мог бы сделать наследование двоичным, а не линейным (чтобы ограничить глубину создания шаблонов и глубину дерева наследования).


Кроме того, есть 4 причины, по которым я знаю, чтобы SFINAE включал лямбды.

Во-первых, с новым std::functionВы можете перегрузить функцию на несколько разных сигнатур обратного вызова.

Во-вторых, вышеупомянутый трюк.

В-третьих, каррирование объекта функции, где он оценивает, когда у него есть правильное число и тип аргументов.

Далее автоматическая распаковка кортежей и тому подобное. Если я использую стиль передачи продолжения, я могу спросить переданный в продолжении, примет ли он, что кортеж распакован, или будущее разделено, и т. Д.

7

Другие решения

У общей лямбды может быть только одно тело, поэтому SFINAE не будет здесь очень полезен.

Одним из решений будет упаковка вызова в класс, который может хранить результат и специализируется на void возвращаемый тип, инкапсулирующий void специальная обработка от вашего лямбда. С очень небольшими накладными расходами вы можете сделать это, используя возможности библиотеки потоков:

auto gL =
[](auto&& func, auto&&... params)
{
// start a timer
using Ret = decltype(std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...));
std::packaged_task<Ret()> task{[&]{
return std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...); }};
auto fut = task.get_future();
task();
// stop timer and print elapsed time
return fut.get();
};

Если вы хотите избежать накладных расходов packaged_task а также futureлегко написать свою собственную версию:

template<class T>
struct Result
{
template<class F, class... A> Result(F&& f, A&&... args)
: t{std::forward<F>(f)(std::forward<A>(args)...)} {}
T t;
T&& get() { return std::move(t); }
};
template<>
struct Result<void>
{
template<class F, class... A> Result(F&& f, A&&... args)
{ std::forward<F>(f)(std::forward<A>(args)...); }
void get() {}
};

auto gL =
[](auto&& func, auto&&... params)
{
// start a timer
using Ret = decltype(std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...));
Result<Ret> k{std::forward<decltype(func)>(func),
std::forward<decltype(params)>(params)...};
// stop timer and print elapsed time
return k.get();
};
2

Использование SFINAE состоит в том, чтобы удалить перегрузку или специализацию из набора кандидатов при разрешении данной функции или шаблона. В вашем случае у нас есть лямбда — это функтор с одним operator(), Перегрузки нет, поэтому нет смысла использовать SFINAE1. Тот факт, что лямбда является общим, что делает его operator() шаблон функции, не меняет этот факт.

Однако на самом деле вам не нужно различать различные типы возвращаемых данных. Если func возвращается void для приведенных аргументов вы все еще можете return Это. Вы просто не можете назначить его временному. Но вы не должны делать это либо:

auto time_func = [](auto&& func, auto&&... params) {
RaiiTimer t;
return std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...);
};

Просто напишите RaiiTimer чей конструктор запускает таймер, а чей деструктор останавливает его и печатает результат. Это будет работать независимо от funcтип возврата.

Если вам нужно что-то более сложное, чем это, то это один из тех случаев, когда вы должны предпочитаю функтор, а не лямбду.


1На самом деле, как отмечает Якк, SFINAE все еще может пригодиться, чтобы проверить, является ли ваша функция вызываемым периодом, что не является проблемой, которую вы пытаетесь решить — поэтому в этом случае все еще не очень полезно.

2
По вопросам рекламы [email protected]