The task is to create a single-argument function that forwards all types apart from one (Foo), which it converts (to Bar).
(Let us assume there exists a conversion from Foo to Bar).
Here is the usage scenario:
template<typename Args...>
void f( Args... args )
{
g( process<Args>(args)... );
}
(I've tried to extract/simplify it from the original context here. -- if I've made a mistake, please someone tell me!)
Here are two possible implementations:
template<typename T>
T&& process(T&& t) {
return std::forward<T>(t);
}
Bar process(Foo x) {
return Bar{x};
}
And...
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
template <typename T>
Bar process(Foo x) {
return Bar{x};
}
I have it on good authority (here) that the second one is preferable.
However, I can't understand the given explanation. I think this is delving into some of the darkest corners of C++.
I think I'm missing machinery necessary to understand what's going on. Could someone explain in detail? If it is too much digging, could anyone recommend a resource for learning the necessary prerequisite concepts?
EDIT: I would like to add that in my particular case the function signature is going to match one of the typedef-s on this page. That is to say, every argument is going to be either PyObject*
(with PyObject
being an ordinary C struct) or some basic C type like const char*
, int
, float
. So my guess is that the lightweight implementation may be most appropriate (I'm not a fan of over-generalising). But I am really interested in acquiring the right mindset to solve such problems as these.
I sense a minor misconception in your understanding of the use case you are facing.
First of all, this is a function template:
struct A
{
template <typename... Args>
void f(Args... args)
{
}
};
And this is not a function template:
template <typename... Args>
struct A
{
void f(Args... args)
{
}
};
In the former definition (with a function template) the argument type deduction takes place. In the latter, there is no type deduction.
You aren't using a function template. You're using a non-template member function from a class template, and for this particular member function its signature is fixed.
By defining your trap
class like below:
template <typename T, T t>
struct trap;
template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args);
};
and referring to its member function like below:
&trap<decltype(&Base::target), &Base::target>::call;
you end up with a pointer to a static non-template call
function with a fixed signature, identical to the signature of the target
function.
Now, that call
function serves as an intermediate invoker. You will be calling the call
function, and that function will call the target
member function, passing its own arguments to initialize target
's parameters, say:
template <typename R, typename... Args, R(Base::*t)(Args...)>
struct trap<R(Base::*)(Args...), t>
{
static R call(Args... args)
{
return (get_base()->*t)(args...);
}
};
Suppose the target
function used to instantiate the trap
class template is defined as follows:
struct Base
{
int target(Noisy& a, Noisy b);
};
By instantiating the trap
class you end up with the following call
function:
// what the compiler *sees*
static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, b);
}
Luckily, a
is passed by reference, it is just forwarded and bound by the same kind of reference in the target
's parameter. Unfortunately, this doesn't hold for the b
object - no matter if the Noisy
class is movable or not, you're making multiple copies of the b
instance, since that one is passed by value:
the first one: when the call
function is invoked itself from an external context.
the second one: to copy b
instance when calling the target
function from the body of call
.
DEMO 1
This is somewhat inefficient: you could have saved at least one copy-constructor call, turning it into a move-constructor call if only you could turn the b
instance into an xvalue:
static int call(Noisy& a, Noisy b)
{
return get_base()->target(a, std::move(b));
// ~~~~~~~~~~~^
}
Now it would call a move constructor instead for the second parameter.
So far so good, but that was done manually (std::move
added knowing that it's safe to apply the move semantics). Now, the question is, how could the same functionality be applied when operating on a parameter pack?:
return get_base()->target(std::move(args)...); // WRONG!
You can't apply std::move
call to each and every argument within the parameter pack. This would probably cause compiler errors if applied equally to all arguments.
DEMO 2
Fortunately, even though Args...
is not a forwarding-reference, the std::forward
helper function can be used instead. That is, depending on what the <T>
type is in std::forward<T>
(an lvalue reference or a non-lvalue-reference) the std::forward
will behave differently:
for lvalue references (e.g. if T
is Noisy&
): the value category of the expression remains an lvalue (i.e. Noisy&
).
for non-lvalue-references (e.g. if T
is Noisy&&
or a plain Noisy
): the value category of the expression becomes an xvalue (i.e. Noisy&&
).
Having that said, by defining the target
function like below:
static R call(Args... args)
{
return (get_base()->*t)(std::forward<Args>(args)...);
}
you end up with:
static int call(Noisy& a, Noisy b)
{
// what the compiler *sees*
return get_base()->target(std::forward<Noisy&>(a), std::forward<Noisy>(b));
}
turning the value category of the expression involving b
into an xvalue of b
, which is Noisy&&
. This lets the compiler pick the move constructor to initialize the second parameter of the target
function, leaving a
intact.
DEMO 3 (compare the output with DEMO 1)
Basically, this is what the std::forward
is for. Usually, std::forward
is used with a forwarding-reference, where T
holds the type deduced according to the rules of type deduction for forwarding references. Note that it always requires from you to pass over the <T>
part explicitly, since it will apply a different behavior depending on that type (not depending on the value category of its argument). Without the explicit type template argument <T>
, std::forward
would always deduce lvalue references for arguments referred to through their names (like when expanding the parameter pack).
Now, you wanted to additionally convert some of the arguments from one type to another, while forwarding all others. If you don't care about the trick with std::forward
ing arguments from the parameter pack, and it's fine to always call a copy-constructor, then your version is OK:
template <typename T> // transparent function
T&& process(T&& t) {
return std::forward<T>(t);
}
Bar process(Foo x) { // overload for specific type of arguments
return Bar{x};
}
//...
get_base()->target(process(args)...);
DEMO 4
However, if you want to avoid the copy of that Noisy
argument in the demo, you need to somehow combine std::forward
call with the process
call, and pass over the Args
types, so that std::forward
could apply proper behavior (turning into xvalues or not doing anything). I just gave you a simple example of how this could be implemented:
template <typename T, typename U>
T&& process(U&& u) {
return std::forward<T>(std::forward<U>(u));
}
template <typename T>
Bar process(Foo x) {
return Bar{x};
}
//...
get_base()->target(process<Args>(args)...);
But this is just one of the options. It can be simplified, rewritten, or reordered, so that std::forward
is called before you call the process
function (your version):
get_base()->target(process(std::forward<Args>(args))...);
DEMO 5 (compare the output with DEMO 4)
And it will work fine as well (that is, with your version). So the point is, the additional std::forward
is just to optimize your code a little, and the provided idea was just one of the possible implementations of that functionality (as you can see, it brings about the same effect).
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