Semantics for wrapped objects: reference/value by

2019-02-16 02:38发布

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

0条回答
登录 后发表回答