Когда я назначаю лямбду явно типизированной переменной (например, когда она является рекурсивной, чтобы захватить функцию внутри себя), я использую std::function
,
Рассмотрим эту глупую функцию «подсчета битов» в качестве примера:
std::function<int(int)> f;
f = [&f](int x){ return x ? f(x/2)+1 : 0; };
Как насчет случая, когда мы используем автоматический параметр для обобщения x
, как введено в C ++ 14 универсальная лямбда?
std::function<int(???)> f;
f = [&f](auto x){ return x ? f(x/2)+1 : 0; };
Очевидно, я не могу разместить auto
в function
параметры типа.
Есть ли возможность определить класс функтора достаточно обобщенно, чтобы охватить конкретный случай выше, но все еще используя лямбду для определения функции?
(Не обобщайте это, принимайте только один автоматический параметр и жестко кодируйте возвращаемое значение.) Вариант использования будет для сценария, подобного описанному выше: захват самой функции посредством ссылки для рекурсивных вызовов.
Вы можете создать лямбду, которая вызывает себя, передавая ее себе в качестве параметра:
auto f = [](auto self, auto x) -> int {
return x ? self(self, x / 2) + 1 : 0;
};
std::cout << f(f, 10);
Затем вы можете захватить эту лямбду в другую лямбду, так что вам не придется беспокоиться о передаче ее самой себе:
auto f2 = [&f](auto x) {
return f(f, x);
};
std::cout << f2(10);
Вот быстрый рекурсивный двигатель на основе y-комбинатора:
template<class F>
struct recursive_t {
F f;
// note Self must be an lvalue reference. Things get
// strange if it is an rvalue:
// invoke makes recursive ADL work a touch better.
template<class Self, class...Args>
friend auto invoke( Self& self, Args&&...args )
-> decltype( self.f( self, std::declval<Args>()... ) )
{
return self.f( self, std::forward<Args>(args)... );
}
// calculate return type using `invoke` above:
template<class Self, class...Args>
using R = decltype( invoke( std::declval<Self>(), std::declval<Args>()... ) );
template<class...Args>
R<recursive_t&, Args...> operator()(Args&&...args)
{
return invoke( *this, std::forward<Args>(args)... );
}
template<class...Args>
R<recursive_t const&, Args...> operator()(Args&&...args)const
{
return invoke( *this, std::forward<Args>(args)... );
}
};
template<class F>
recursive_t< std::decay_t<F> > recurse( F&& f )
{
return {std::forward<F>(f)};
}
Теперь вы можете сделать:
auto f = recurse( [](auto&& f, auto x){ return x ? f(x/2)+1 : 0; } );
и вы получите рекурсивную лямбду, которая не имеет &
захват (который ограничивает его использование текущей областью).
Захват std::function
по ссылке означает, что время жизни вашей лямбды является текущей областью, и каждый рекурсивный вызов требует прохождения стирания типа (блокирование любой возможной оптимизации, такой как хвостовая рекурсия, поверх рекурсивного вызова). То же самое относится и к другим аналогичным решениям.
Использование recursive_t
требуется, а не использование лямбды, потому что лямбда не может называть себя внутри себя.
Лямбда-версия несколько проще в реализации. Обратите внимание, что вам понадобится функция другого типа для непостоянных и неизменных лямбд:
template<class F>
auto recurse( F&& f ) {
return [f=std::forward<F>(f)](auto&&...args){
return f(f, decltype(args)(args)...);
};
};
recursive_t
работает как:
auto fib = recurse( [](auto&& fib, int x){ if (x<2) return 1; return fib(x-1)+fib(x-2); } );
Лямбда-версия работает так:
auto fib = recurse( [](auto&& self, int x){ if (x<2) return 1; return self(self, x-1)+self(self,x-2); } );
что я лично считаю более неловким.
Также сложнее описать тип recurse
, Для recursive_t
версия, recurse
имеет тип:
((A->B)->A->B)->(A->B)
что неудобно, но конечный тип.
Лямбда-версия сложнее. Тип аргумента функции для recursive
имеет тип:
F:= F->A->B
который досадно бесконечен, а затем recurse
имеет тип
F->A->(A->B)
который наследует бесконечность.
Во всяком случае, recurse
возвращаемое значение может быть сохранено в обыденном std::function
или не хранится в каком-либо стертом типе контейнера.