Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Selective forwarding function

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.

like image 285
P i Avatar asked Oct 31 '22 11:10

P i


1 Answers

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::forwarding 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).

like image 57
Piotr Skotnicki Avatar answered Nov 15 '22 17:11

Piotr Skotnicki