This is a question on lambda overload sets and perfect forwarding and somewhat of a followup to a comment. For more context of how this is used see another related question.
I have some questions on the below code snippet.
Q1: For lambda overloads, I was using overload(Fs...) -> overload<Fs...>
from this post, but then in this answer I saw overload(Fs&&...) -> overload<std::decay_t<Fs>...>
. In what situations is this difference relevant?
Q2: Why would you want to define the identity
function below with return decltype(x)(x)
and not just return x
?
Q3: Can we consider foo(convert(std::forward<Args>(args))...)
as perfect forwarding (for all not-converted arguments) just like foo(std::forward<Args>(args)...)
?
#include <utility>
#include <iostream>
/////////////////////////////////////////////////////////////////////////////////
struct Foo {
virtual ~Foo() = default;
};
struct FooA: public Foo {
static void foo(const FooA&, int) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};
struct FooB: public Foo {
static void foo(int, const FooB&) { std::cout << __PRETTY_FUNCTION__ << std::endl; }
};
/////////////////////////////////////////////////////////////////////////////////
template<class...Fs>
struct overload:Fs... {
using Fs::operator()...;
};
// Q1: In what situations is needed over `overload(Fs...) -> overload<Fs...>`?
template<class...Fs>
overload(Fs&&...) -> overload<std::decay_t<Fs>...>;
/////////////////////////////////////////////////////////////////////////////////
// Q2: What is the purpose of `return decltype(x)(x)` over `return x`?
auto identity=[](auto&&x)->decltype(x){return decltype(x)(x);};
template<typename SpecificFoo, typename... Args>
void bar(Args&&... args) {
auto convert = overload{
[](const Foo& f){return dynamic_cast<const SpecificFoo&>(f);},
identity
};
// Q3: Is our definition of `convert` "perfectly forwarding", like if we just called
// `foo(std::forward<Args>(args)...)`, or in what situations might this not do the
// same thing (for not-converted types)?
SpecificFoo::foo(convert(std::forward<Args>(args))...);
}
/////////////////////////////////////////////////////////////////////////////////
int main() {
{
FooA specific_foo;
const Foo& foo {specific_foo};
// assume we only have access to foo when calling bar
bar<FooA>(foo, 23);
}
{
FooB specific_foo;
const Foo& foo {specific_foo};
// assume we only have access to foo when calling bar
bar<FooB>(42, foo);
}
}
When at least one argument is in fact an lvalue (like
identity
, in fact). In which case the correspondingFi
is aT&
, i.e. an lvalue reference. And one can't list an lvalue reference as a base of any class, sostd::decay
is required to remove all reference and cv-qualifiers. When the deduction guide takes arguments by value, it's automatically a non-issue. This is because template argument deduction for value types already "decays" the types.If you wonder which one to use, then I'd say the one with less clutter is objectively better. The use of
std::decay_t
is for getting the same behavior one would get with the by-value version, so one may as well use that.This is a form of forwarding. Since the return type of the lambda is declared to be
decltype(x)
, we need the cast to make sure it binds correctly to an rvalue reference. Because in the casedecltype(x) = T&&
, it will not bind tox
alone, which is an lvalue.Yes. The arguments to
bar
already bound to references.convert
lets those references pass through with value category preserved, so it's forwarding indeed.