Selective Forward Function

The task is to create a function with one argument that forwards all types except one (Foo), which it converts (to Bar).

(Suppose there is a conversion from Foo to Bar).

Here is a use case:

template<typename Args...> void f( Args... args ) { g( process<Args>(args)... ); } 

(I tried to extract / simplify it from the original context here . - If I made a mistake, please tell me someone!)

Here are two possible implementations:

 template<typename T> T&& process(T&& t) { return std::forward<T>(t); } Bar process(Foo x) { return Bar{x}; } 

A...

 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 good authority ( here ), which is preferable to the second.

However, I cannot understand this explanation. I think this delves into some of the darkest corners of C ++.

I think that I lack the equipment necessary to understand what is happening. Can someone explain in detail? If this is too much, can you recommend a resource for exploring the necessary preliminary concepts?

EDIT: I would like to add that in my particular case, the signature of the function will match one of typedef-s on this page . That is, each argument will be either PyObject* (with PyObject is a regular C-structure), or some basic C-type of type const char* , int , float . Therefore, I suggest that a lightweight implementation may be most appropriate (I'm not a fan of over-generalizing). But I'm really interested in getting the right mindset to solve problems like these.

+5
source share
2 answers

I feel a slight misconception in your understanding of the case that you encountered.

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 first definition (with the function template), the deduction of the argument type occurs. In the latter case, there is no type subtraction.

You are not using a function template. You are using a non-template member function from a class template, and for this particular member function its signature is fixed.

Defining your trap class as shown 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 as shown below:

 &trap<decltype(&Base::target), &Base::target>::call; 

you get a pointer to a static non-template call function with a fixed signature, identical to the signature of the target function.

Now this call function serves as an intermediate call. You will call function, and this function will call the target member function, passing its own arguments to initialize the target 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...); } }; 

Assume that the target function used to instantiate the template for the trap class is defined as follows:

 struct Base { int target(Noisy& a, Noisy b); }; 

By creating an instance of the trap class, you get the following call function:

 // what the compiler *sees* static int call(Noisy& a, Noisy b) { return get_base()->target(a, b); } 

Fortunately, a is passed by reference, it is simply redirected and attached to the same type of links in the target parameter. Unfortunately, this does not apply to object b - regardless of whether the Noisy class is movable or not, you make several instances of instance b , since this one is passed by value:

  • first: when the call function is called itself from an external context.

  • second: copy instance b when calling the target function from the call body.

Demo 1

This is somewhat inefficient: you could save at least one constructor invocation by turning it into a move constructor invocation if you could turn an instance of b into an x โ€‹โ€‹value:

 static int call(Noisy& a, Noisy b) { return get_base()->target(a, std::move(b)); // ~~~~~~~~~~~^ } 

Now it will call the move constructor instead of the second parameter.

So far, everything is fine, but it was done manually ( std::move added, knowing that it is safe to apply the semantics of movement). Now the question is, how can you use the same functionality when working with the parameter package ?:

 return get_base()->target(std::move(args)...); // WRONG! 

You cannot apply the call to std::move to each argument in a parameter package. This will probably lead to compiler errors if they apply equally to all arguments.

Demo 2

Fortunately, although Args... not a referral link, the helper function std::forward can be used instead. That is, depending on whether the type <T> is in std::forward<T> (lvalue reference or non-lvalue-reference), std::forward will behave differently:

  • for lvalue references (for example, if T is Noisy& ): the category of expression values โ€‹โ€‹remains an lvalue (i.e. Noisy& ).

  • for non-lvalue references (for example, if T is Noisy&& or plain Noisy ): the value category of the expression becomes xvalue (i.e. Noisy&& ).

Having said that, defining the target function as shown below:

 static R call(Args... args) { return (get_base()->*t)(std::forward<Args>(args)...); } 

in the end it will turn out:

 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 category of expression values โ€‹โ€‹from b into the value of x b , which is equal to Noisy&& . This allows the compiler to select a move constructor to initialize the second parameter of the target function, leaving a intact.

DEMO 3 (compare the output with DEMO 1)

In principle, std::forward serves for this. Typically, std::forward used with a referral link, where T contains the type deduced according to the type inference rules for referring links. Please note that you always need to explicitly pass the <T> , since it will use different behavior depending on this type (independent of the category of values โ€‹โ€‹of its argument). Without an explicit template argument of type <T> , std::forward always displays lvalue references for arguments passed through their names (for example, when expanding the parameter package).

Now you would like to additionally convert some arguments from one type to another, sending all the others. If you are not interested in the trick with the arguments std::forward ing from the parameter package, and this is normal, if you always call the copy constructor, then your version is fine :

 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 copying this Noisy argument into the demo, you need to somehow combine the std::forward call with the process call, and go through Args so that std::forward can apply the correct behavior (turning into x values โ€‹โ€‹or nothing not to do). I just gave you a simple example of how this can 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 only one option. It can be simplified, overwritten, or reordered, so std::forward is called before calling the process function (your version):

 get_base()->target(process(std::forward<Args>(args))...); 

DEMO 5 (compare the output with DEMO 4)

And this will work well (i.e. with your version). So the fact is that the additional std::forward just optimizes your code a little, and the idea presented was only one of the possible implementations of this functionality (as you can see, this leads to the same effect).

+2
source

Will the first part of version 2 not be enough? only:

 template <typename T, typename U> T&& process(U&& u) { return std::forward<T>(std::forward<U>(u)); } 

Given an example of use with an existing transformation (constructor for "Bar" from "Foo"), for example:

 struct Foo { int x; }; struct Bar { int y; Bar(Foo f) { y = fx; } }; int main() { auto b = process<Bar>(Foo()); // b will become a "Bar" auto i = process<int>(1.5f); } 

In any case, you must specify the first parameter of the template (type for conversion), since the compiler cannot output it. Therefore, he knows what type you expect, and build a temporary object of type "Bar", because there is a constructor.

0
source

Source: https://habr.com/ru/post/1210769/


All Articles