0

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.

alfC
  • 14,261
  • 4
  • 67
  • 118
  • 2
    I'd argue that a) value semantics (copy = default, in most of the Standard Library) b) dangling references are dangerous, therefore you should need to explicitly introduce them. – dyp Nov 19 '13 at 19:42
  • 1
    The behavior of discarding references except when given a `reference_wrapper` might be more complicated to implement, but I'd argue it makes use a lot easier, because you don't have to worry about dangling references, unless you specifically say so by using `std::ref` – Praetorian Nov 19 '13 at 19:43
  • @Praetorian and DyP, for a long time also I was under the impression that dangling references will ultimately bite me but so far I found that this is very natural indeed, and I have to worry less about involuntary copies. Maybe my usage is biased because I handle a lot of *unmodifiable* big objects (things that are expensive to modify than to copy them, for example interpolations). Maybe it has to do with having a more functional programming style in the context. – alfC Nov 19 '13 at 20:04
  • With your `wrap` solution, there is no way to auto-detect that you want to store by-value without moving into said values. With the `make_pair` solution, everything that `wrap` does can be done, plus you can store by-value without moving. – Yakk - Adam Nevraumont Nov 19 '13 at 20:14
  • 1
    @Yakk Wouldn't a function `copy` resolve this? `template T copy(T const& p) { return T(p); }`, `wrap(copy(a))` -- vs. `make_pair(move(a))` – dyp Nov 19 '13 at 20:17
  • @Yakk, if I understand correctly `make_pair` has the opposite limitation "there is no way to auto-detect that you want to store by-*ref* without *reference-wrapping* into said values". what is the problem with `make_wrap(std::move(c))`? or are you talking about in copy constructor? In which case yes, you are right but the limiation is still symmetric. – alfC Nov 19 '13 at 20:22
  • @DyP extra and needless `move` there, but yes. – Yakk - Adam Nevraumont Nov 19 '13 at 20:23
  • 1
    @alfC "Without doing X" where X has no overhead is different than "you cannot do Y", or "you can only do Y, and Y implies unavoidable overhead". The limitation is not symmetric. There is no way to say "I want to store by-value in `wrap`" without doing both a `copy` (into a temporary) and then a `move`. Regardless, `make_pair` was designed prior to rvalue references. – Yakk - Adam Nevraumont Nov 19 '13 at 20:24
  • @Yakk Oh, yeah, after binding to the rvalue ref function parameter the move cannot be elided. I guess a bit of metaprogramming in `wrapper` and something like `template to_copy copy(T const&);` could eliminate the move. – dyp Nov 19 '13 at 20:27
  • @Yakk, I don't understand why `move(a)` would need a copy in general and if that is more costly than the `ref(a)` call, but I believe you. I know that `make_pair` was designed before rvalues, I wonder if in the light of rvalues it could have been done differently, I think in the reimplementation of make_pair they had a choice still in the overload(???) of `make_pair(T1&&, T2&&)` and they decided one way. – alfC Nov 19 '13 at 20:35
  • @alfC For *"Are you wondering why a certain C++ language or library feature works the way it does?"*, you could try asking [on the isocpp forums](http://isocpp.org/forums). But as I said earlier, and might need to emphasize: If `make_pair` created pairs storing references, it would be *inconsistent* to the rest of the Standard Library functions. – dyp Nov 19 '13 at 20:40
  • @Yakk, if I understand the limitation that could be solved with the code I posted in the EDIT2 with a sort of generalized construction, so things like `wrap b(wrap(a))` can be handled. I could be missing something (yes, this enters into nasty waters of universal references and overloading rules). – alfC Nov 19 '13 at 21:09

0 Answers0