3
votes

I wish to create a wrapper around std::make_pair that takes a single argument and uses that argument to make the first and second members of the pair. Furthermore, I wish to take advantage of move semantics.

Naively, we might write (ignoring return types for clarity),

template <typename T>
void foo(T&& t)
{
  std::make_pair(std::forward<T>(t),
                 std::forward<T>(t));
}

but this is unlikely to do what we want.

What we want is:

  • In the case where foo is called with a (const) lvalue reference argument, we should pass that (const) reference on to std::make_pair unmodified for both arguments.
  • In the case where foo is called with an rvalue reference argument, we should duplicate the referenced object, then call std::make_pair with the original rvalue reference as well as an rvalue reference to the newly created object.

What I've come up with so far is:

template <typename T>
T forward_or_duplicate(T t)
{
  return t;
}

template <typename T>
void foo(T&& t)
{
  std::make_pair(std::forward<T>(t),
                 forward_or_duplicate<T>(t));
}

But I'm reasonably sure it's wrong.

So, questions:

  1. Does this work? I suspect not in that if foo() is called with an rvalue reference then T's move constructor (if it exists) will be called when constructing the T passed by value to forward_or_duplicate(), thus destroying t.

  2. Even if it does work, is it optimal? Again, I suspect not in that T's copy constructor will be called when returning t from forward_or_duplicate().

  3. This seems like a common problem. Is there an idiomatic solution?

2
I think you'd be able to do it with simply std::make_pair(t, std::forward<T>(t)) if the ordering between parameter evaluation was defined (unfortunately, it isn't). - Cameron
@KerrekSB: Hah, didn't think of that. You should submit an answer :-) - Cameron
I actually don't see anything wrong with forward_or_duplicate. - T.C.
@KerrekSB I don't see it. make_pair takes arguments by reference, so the move happens inside it, and the copy happens before you enter make_pair. - T.C.
@KerrekSB it forwards lvalues, and duplicates rvalues. - T.C.

2 Answers

5
votes

So, questions:

  1. Does this work? I suspect not in that if foo() is called with an rvalue reference then T's move constructor (if it exists) will be called when constructing the T passed by value to forward_or_duplicate(), thus destroying t.

No, t in foo is an lvalue, so constructing the T passed by value to forward_or_duplicate() from t calls the copy constructor.

  1. Even if it does work, is it optimal? Again, I suspect not in that T's copy constructor will be called when returning t from forward_or_duplicate().

No, t is a function parameter, so the return implicitly moves, and doesn't copy.

That said, this version will be more efficient and safer:

template <typename T>
T forward_or_duplicate(std::remove_reference_t<T>& t)
{
  return t;
}

If T is an lvalue reference, this results in the same signature as before. If T is not a reference, this saves you a move. Also, it puts T into a non-deduced context, so that you can't forget to specify it.

0
votes

Your exact code works. Slight variations of it (ie, not calling make_pair but some other function) result in unspecified results. Even if it appears to work, subtle changes far from this line of code (which are locally correct) can break it.

Your solution isn't optimal, because it can copy a T twice, even when it works, when it only needs to copy it once.


This is by far the easiest solution. It doesn't fix the subtle breaks caused by code elsewhere changing, but if you are really calling make_pair that is not a concern:

template <typename T>
void foo(T&& t) {
  std::make_pair(std::forward<T>(t),
             static_cast<T>(t));
}

static_cast<T>(t) for a deduced type T&& is a noop if T&& is an lvalue, and a copy if T&& is an rvalue.

Of course, static_cast<T&&>(t) can also be used in place of std::forward<T>(t), but people don't do that either.

I often do this:

template <typename T>
void foo(T&& t) {
  T t2 = t;
  std::make_pair(std::forward<T>(t),
             std::forward<T>(t2));
}

but that blocks a theoretical elision opportunity (which does not occur here).

In general, calling std::forward<T>(t) on the same function call as static_cast<T>(t) or any equivalent copy-or-forward function is a bad idea. The order in which arguments are evaluated is not specified, so if the argument consuming std::forward<T>(t) is not of type T&&, and its constructor sees the rvalue T and moves state out of it, the static_cast<T>(t) could evaluate after the state of t has been ripped out.

This does not happen here:

template <typename T>
void foo(T&& t) {
  T t2 = t;
  std::make_pair(std::forward<T>(t),
             std::forward<T>(t2));
}

because we move the copy-or-forward to a different line, where we initialize t2.

While T t2=t; looks like it always copies, if T&& is an lvalue reference, T is also an lvalue reference, and int& t2 = t; doesn't copy.