1

Why these asserts work for the below code? The universal reference should bind to l-value reference run(T& a), and copy an object b from a. However both objects addresses "a" and "b" are the same in the run() function. Tested with C++11/14/17/2a gcc-9.2 and clang++-6.0. What part of the standard says this is valid? did not find anything related.

#include <cassert>
#include <utility>

template <typename T>
void run(T&& a)
{
  T b {std::forward<T>(a)};
  ++b;
  assert(b == a);
  assert(&a == &b);
}

int main()
{
  int value {10};
  run(value); // asserts work, not expected
  // run(std::move(value)); // the asserts don't work as expected
}
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
AdvSphere
  • 986
  • 7
  • 15

2 Answers2

2

However both objects addresses "a" and "b" are the same in the run() function.

When being passed an lvalue, T is deduced as lvalue-reference, i.e. int&. (int& && collapses to int&, so the type of function parameter a is int&.) Then b is declared as a reference binding to a.

When being passed an rvalue, T is deduced as int. (So the type of function parameter a is int&&.) Then b is declared as an independent variable copied from a.

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • That's interesting. I was not expecting the type `T` to change, but instead the type of the argument. Semantic wise, is clear the template is not a reference. – AdvSphere Jun 20 '20 at 01:05
  • @AdvSphere, `&&` is an rvalue reference. A forwarding reference (universal reference) isn't truly its own entity, just the result of `& &&` becoming `&` and `&& &&` (or ` &&`) becoming `&&`. – chris Jun 20 '20 at 01:14
  • @AdvSphere The type of `T` changes, which yields the change of the function parameter type. – songyuanyao Jun 20 '20 at 01:25
  • Thanks for the clarification did some more reading and makes sense. std::forward needs to include the `&` for lvalue, therefore the whole deduction of type `T` for the function includes the reference. – AdvSphere Jun 20 '20 at 01:27
  • @AdvSphere Yes, forwarding reference and `std::forward` have to work together; then we can forward arguments reserving their value categories. – songyuanyao Jun 20 '20 at 01:38
1

In run(value), value is an lvalue, and it needs to match with T&&. Lvalues cannot bind to rvalue references, so T = int and T = int&& won’t do, as then T&& = int&&. The only thing that works is T = int&. Due to reference collapsing, an rvalue reference to lvalue reference is an lvalue reference, so the instantiation of run looks like:

template<>
void run<int&>(int &a) {
    int &b{a}; // expanding std::forward
    ++b;
    assert(b == a);
    assert(&b == &a);
}

Obviously, the assertions always pass. Now, for run(std::move(value)), the argument is indeed an rvalue, and you get T = int. Then

template<>
void run<int>(int &&a) {
    int b{std::move(a)};
    ++b;
    assert(b == a);
    assert(&b == &a);
}

This of course fails. Perhaps you should replace

T b{std::forward<T>(a)};

with

std::decay_t<T> b{std::forward<T>(a)};

This will remove references from T (ensuring b is a new (copied/moved) object) and also handle arrays and functions (by making b a pointer even if a isn’t).

Doubt you need them, but [temp.deduct.call]/3 talks about the template deduction of forwarding references, and [dcl.init.list]/3.9 says that list-initializing a reference just binds it to the element of initializer list. Also [forward], well, explains std::forward<T>. Basically, if T is an lvalue reference, then std::forward<T>(x) is an lvalue, and otherwise an xvalue (a kind of rvalue). (Basically it’s a conditional std::move.)

HTNW
  • 27,182
  • 1
  • 32
  • 60
  • If all you need is a copy, `auto b = a;` seems the most straightforward choice. That does the decay without having to be explicit. – chris Jun 20 '20 at 01:54