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);
}
}
run it
In what situations is this difference relevant?
When at least one argument is in fact an lvalue (like identity
, in fact). In which case the corresponding Fi
is a T&
, i.e. an lvalue reference. And one can't list an lvalue reference as a base of any class, so std::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.
Why would you want to define the identity function below with return decltype(x)(x) and not just return x
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 case decltype(x) = T&&
, it will not bind to x
alone, which is an lvalue.
Can we consider
foo(convert(std::forward<Args>(args))...)
as perfect forwarding (for all not-converted arguments) just likefoo(std::forward<Args>(args)...)
Yes. The arguments to bar
already bound to references. convert
lets those references pass through with value category preserved, so it's forwarding indeed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With