In recent times I am using often a natural idiom I "discovered" in C++11 that is that wrapped object can automatically hold reference when this is possible. The main question here will be about the comparison with the behavior of this "idiom" to other behaviors in the standard (see below).
For example:
template<class T>
struct wrap{
T t;
};
template<class T> wrap<T> make_wrap(T&& t){
return wrap{std::forward<T>(t)};
}
In this way for the code
double a = 3.14
double const c = 3.14
I get,
typeid( make_wrap(3.14) ) --> wrap<double>
typeid( make_wrap(a) ) --> wrap<double&>
typeid( make_wrap(c) ) --> wrap<double const&>
which if I am careful (with dangling references) I can handle pretty well. And if I want to avoid references I do:
typeid( make_wrap(std::move(a)) ) --> wrap<double> // noref
typeid( make_wrap(std::move(c)) ) --> wrap<double const> // noref
So, this behavior seems natural in C++11.
Then I went back to std::pair
and std::make_pair
and somehow I expected that they used this new seemly natural behavior, but apparently the behavior is "more traditional". So for example:
typeid( std::make_pair(3.14, 3.14) ) --> std::pair<double, double>
typeid( std::make_pair(a, a) ) --> std::pair<double, double> // noref
typeid( std::make_pair(c, c) ) --> std::pair<double, double> // noref
and for references:
typeid( std::make_pair(std::ref(a), std::ref(a) ) ) --> std::pair<double&, double&> // ref
typeid( std::make_pair(std::ref(c), std::ref(c) ) ) --> std::pair<double const&, double const&> // const ref
This is documented here: http://en.cppreference.com/w/cpp/utility/pair/make_pair
As you see the two behaviors are "opposite", in some sense std::ref
is the complement to std::move
. So both behaviors are equally flexible at the end, but it seems to me that the std::make_pair
behavior is more difficult to implement and maintain.
The question is: Is the current behavior of std::make_pair
of discarding references by default just a backward compatibility issue? because some historical expectation? or there is a deeper reason that still exists in C++11?
As it is, it looks like this std::make_pair
behavior is much more difficult to implement as it requires specialization for std::ref
(std::reference_wrapper
) and std::decay
and even seems unnatural (in the presence of "C++11 move"). At the same time even if I decide to keep using the first behavior I am affraid that the
behavior will be pretty unexpected with respect to current standards, even in C++11.
In fact I am pretty fond of the first behavior, to the point that the elegant solution maybe to change the prefix make_something
for something like construct_something
in order to mark the difference in behavior. (EDIT: one of the comments suggested to look at std::forward_as_tuple
, so another name convention could be forward_as_something
). Regarding naming, the situation is not clear cut when pass-by-value, pass-by-ref is mixed in the construction of the object.
EDIT2: This is an edit just to answer a @Yakk's about being able to "copy" wrap object with different ref/value properties. This is not part of the question and it is just experimental code:
template<class T>
struct wrap{
T t;
// "generalized constructor"? // I could be overlooking something important here
template<class T1 = T> wrap(wrap<T1> const& w) : t(std::move(w.t)){}
wrap(T&& t_) : t(std::move(t)){} // unfortunately I now have to define an explicit constructor
};
This seems to allow me to copy between unrelated types wrap<T&>
and wrap<T>
:
auto mw = make_wrap(a);
wrap<double const&> copy0 =mw;
wrap<double&> copy1 = mw; //would be an error if `a` is `const double`, ok
wrap<double> copy2 = mw;
EDIT3: This edit is to add a concrete example in which the traditional reference deduction can fail depend on a "protocol". The example is based in the use of Boost.Fusion.
I discovered how much the implicit conversion from reference to value can depend on the convention. For example the good old Boost.Fusion follows the STL convention of
Fusion's generation functions (e.g. make_list) by default stores the element types as plain non-reference types.
However that relies in the exact "type" that tags the reference, in the case of Fusion was the boost::ref
and in the case of make_pair
is... std::ref
, a completely unrelated class. So, currently, given
double a;
the type of boost::fusion::make_vector(5., a )
is boost::fusion::vector2<double, double>
. Ok, fine.
And the type of boost::fusion::make_vector(5., boost::ref(a) ) ) is
boost::fusion::vector2`. Ok, as documented.
However, surprise, since Boost.Fusion was not written with C++11 STL's in mind we get:
boost::fusion::make_vector(5., std::ref(a) ) )
is of type boost::fusion::vector2<double, std::reference_wrapper<double const> >
. Surprise!
This section was to show that the current STL behavior depends on a protocol (e.g. what class to use to tag references), while the other (what I called "natural" behavior) using std::move
(or more exactly rvalue casting) doesn't depend on a protocol, but it is more native to the (current) language.