Francis Cugler
Francis Cugler

Reputation: 7905

Trying to understand std::forward, std::move a little better

I'm in the process of writing a basic class template. It takes two argument types for its parameters. The idea of the class is to take one type in as a const ref and the other as a ref. The functionality of the class is to convert type A to type B where the object being created will end up being b. I would like to have perfect-forwarding or move semantics a valid part of this class template.


For now here is my current class with just basic types, but plan to expand this to any 2 types with the use of a variadic construction.

#ifndef CONVERTER_H
#define CONVERTER_H

#include <utility>

template<class From, class To>
class Converter {
private:
    From in_;
    To   out_;

public:
    // Would like for From in to be a const (non modifiable) object
    // passed in by perfect forwarding or move semantics and for 
    // To out to be returned by reference with perfect forwarding 
    // or move semantics. Possible Constructor Declarations - Definitions

    // Using std::move
    Converter( From&& in, To&& out ) :
        in_{ std::move( in ) },
        out_{ std::move( out ) } 
    {
        // Code to convert in to out
    }

    // Or using std::forward
    Converter( From&& in, To&& out ) :
        in_{ std::forward<From>( in ) },
        out_{ std::forward<To>( out ) } {

        // Code to convert in to out.
     }        

    // Pseudo operator()... 
    To operator()() {
        return out_;
    }
};

#endif // !CONVERTER_H

In either way that I declare the constructor(s) above with std::move or std::forward this class compiles on its own. Now when I include this and try to instantiate an object above calling its constructor(s)... if I do this:

int i = 10;
float f = 0;  
Converter<int, float> converter( i, f );

This gives me a compiler error in Visual Studio 2017 in both cases.

1>------ Build started: Project: ExceptionManager, Configuration: Debug Win32 ------
1>main.cpp
1>c:\users\skilz80\documents\visual studio 2017\projects\exceptionmanager\exceptionmanager\main.cpp(54): error C2664: 'Converter<unsigned int,float>::Converter(Converter<unsigned int,float> &&)': cannot convert argument 1 from 'unsigned int' to 'unsigned int &&'
1>c:\users\skilz80\documents\visual studio 2017\projects\exceptionmanager\exceptionmanager\main.cpp(54): note: You cannot bind an lvalue to an rvalue reference
1>Done building project "ExceptionManager.vcxproj" -- FAILED.
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

Which is understandable enough {can't bind lvaule to rvalue ref}.


However if I try to use the constructor(s) like this:

int i = 10;
float f = 0;
Converter<int,float> converter( std::move( i ), std::move( f ) );

// Or
Converter<int,float> converter( std::forward<int>( i ), std::forward<float>( f ) );

It compiles and builds regardless if std::move(...) or std::forward<T>(...) is used within the class.


To my better understanding it apparently seems that std::move(...) & std::forward<T>(...) are nearly interchangeable and do the same thing except that std::forward<T>(...) has the extra cast involved.


Right now as this class stands since I'm only showing basic types it would seem more plausible to use std::move, however I may eventually want to use more complex types so I would like to design this ahead of time with that thought in mind, so I'm leaning towards std::forward<T> for the perfect forwarding.


With this being said and to complete this class there are 3 questions coupled together.

  • If I'm using either std::move or std::forward in the class's constructor's member initialize list, why would I have to use them again when instantiating the template class object; wouldn't this be considered redundant? If so how would the constructor look so that the user wouldn't have to use std::move() or std::forward<T>() when calling this constructor?
  • What would be the most general and type safe way to convert A to B within this context?
  • Once the above two are answered with clarity then this last part within this context with the above mentioned standard classes or other similar types would then be what in regards to implementing the operator()() and how would it look with respect to what has already been mentioned above?

To finish things up concerning the 3 coupled questions above my final thought here is that at one point in time of the design process I had thought about using std::any and its related functions as a possible part of its implementation process. I don't know if std::any could be used in this context or not and if so how?


EDIT

Here might be a couple of plausible possible future uses of this class:

vector<int> vecFrom{1,2,3, ...};
set<int>    setTo;

Converter<vector<int>, set<int>> converter( vecFrom, setTo );

Or after expanding...

vector<int>     vecIntFrom{1,2,3, ...};
vector<string>  vecStringFrom{ "a", "b", "c", ... };
map<int,string> mapTo;
Converter<vector<int>, vector<string>, map<int,string> converter( vecIntFrom, vecStringFrom, mapTo );

Upvotes: 9

Views: 1320

Answers (1)

Dr. Watson
Dr. Watson

Reputation: 136

There are a lot of questions here that don't seem concisely answerable to me, but one stands out:

"What is the difference between std::move and std::forward?"

std::move is used to convert an lvalue reference to an rvalue reference, often to transfer the resources held by one lvalue to another.

std::forward is used to distinguish between lvalue and rvalue references, often in the case where a parameter of type rvalue reference is deduced to be an lvalue.

The upshot: If you want to branch based on what kind of reference an object was when passed, use std::forward. If you just want to pilfer the resources of an lvalue by means of converting to an rvalue, use std::move.

For more details, I found the following helpful: http://thbecker.net/articles/rvalue_references/section_01.html

Upvotes: 4

Related Questions