Default Perfect Passing

If you pass a return value to another function directly, the value is passed perfectly, keeping its type and value category. For example:

#include <iostream>
#include <string>

// To avoid overloading ambiguous, we comment out this
//void process(std::string&) {
//   std::cout << "std::string&\n";
//}

void process(std::string&) {
   std::cout << "std::string&\n";
}
void process(const std::string&) {
   std::cout << "const std::string&\n";
}
void process(std::string&&) {
   std::cout << "std::string&&\n";
}

const std::string retConstByValue(std::string& s) {
   return s;
}
std::string retByValue(std::string& s) {
   return s;
}
std::string& retByRef(std::string& s) {
   return s;
}
const std::string& retByConstRef(const std::string& s) {
   return s;
}
std::string&& retByRefRef(std::string&& s) {
   return std::move(s);
}
const std::string&& retConstRefRef(const std::string&& s) {
   return std::move(s);
}

int main() {
   std::string s;
   const std::string cs;

   // Disable perfect forwarding
   process(retConstByValue(s));            // const std::string&
   process(retConstRefRef(std::move(cs))); // const std::string&

   // Perfect forwarding
   process(retByValue(s));             // std::string&&
   process(retByRef(s));               // std::string&
   process(retByConstRef(s));          // const std::string&
   process(retByRefRef(std::move(s))); // std::string&&
}

Therefore, (1) do not mark returned by value with const; (2) do not mark returned non-const rvalue reference with const. Otherwise, it disables move semantics and perfect forwarding.

Universal References with auto&&

Why we need auto&&? Passing a return value later but keeping its type and value category. For example:

#include <iostream>
#include <string>

void process(std::string&&) {
   std::cout << "std::string&&\n";
}
std::string retByValue(std::string& s) {
   return s;
}

int main() {
   std::string s;

   auto&& ret = retByValue(s);

   // do something for ret ...

   process(std::forward<decltype(ret)>(ret)); // std::string&&
}

Type Deduction of auto&&

auto&& r{X};
Value Category of r Decuced expression type of r
decltype((r))
Deduced Declared Type of r
decltype(r)
Expression Type of X
decltype((X))
Value Category of X Declared Type of X
decltype(X)
lvalue Type& lvalue reference
(Type&)
Type& lvalue Type, Type&, Type&&
lvalue Type& rvalue reference
(Type&&)
Type prvalue Type, Type&, Type&&
lvalue Type& rvalue reference
(Type&&)
Type&& xvalue Type, Type&, Type&&

For example:

// forward declaration
std::string retPRValue();
std::string& retLValue();
std::string&& retXValue();
const std::string& retConstLValue();
const std::string&& retConstXValue();

auto&& r1{retPRValue()};     // The type of r1: std::string&&
auto&& r2{retLValue()};      // The type of r2: std::string&
auto&& r3{retXValue()};      // The type of r3: std::string&&
auto&& r4{retConstLValue()}; // The type of r4: const std::string&
auto&& r5{retConstXValue()}; // The type of r5: const std::string&&


std::string s;
const std::string cs;

auto&& r6{s};             // The type of r6: std::string&
auto&& r7{std::move(s)};  // The type of r7: std::string&&
auto&& r8{cs};            // The type of r8: const std::string&
auto&& r9{std::move(cs)}; // The type of r9: const std::string&&

auto&& as Non-Forwarding Reference

Universal References and the Range-Based for loop

std::vector<std::string> coll;

for (const auto& s : coll) {
 ...
}

is equivalent to the following:

std::vector<std::string> coll;

auto&& range = coll; // initialize a universal reference
auto pos = range.begin();
auto end = range.end();

for ( ; pos != end; ++pos ) {
   const auto& s = *pos;
}

The reason we declare range as a universal reference is that we want to be able to bind it to every range so that we can use it twice (once to ask for its beginning, and once to ask for its end) without creating a copy or losing the information about whether or not the range is const.

The loop should work for:

Using the Range-Based for Loop: temporary object

Using the Range-Based for Loop: proxy object

#include <iostream>
#include <string>
#include <vector>

template<class Coll, class T>
void assign(Coll& coll, const T& value) {
   for (auto& elem : coll) {
       elem = value;
   }
}

int main() {
   std::vector<int> coll1{1,2,3};
   std::vector<std::string> coll2{"hello", "world"};
   std::vector<bool> coll3{false, true};

   assign(coll1, 0);
   assign(coll2, "ok");
   assign(coll3, false); // Error
}

This is because: The range-based for loop is equivalent to

auto&& range = coll;
auto pos = coll.begin();
auto end = coll.end();

for (; pos != end; ++pos) {
   auto& elem = *pos; // Error
   elem = value
}

When using std::vector<std::string>, dereferencing iterator, *pos , returns lvalue reference. However, for std::vector<bool>, *pos returns a prvalue of a proxy object.

The solution is:

template<class Coll, class T>
void assign(Coll& coll, const T& value) {
   for (auto&& elem : coll) {
      elem = value;
   }
}

Perfect Forwarding in Lambdas

Using auto&& in C++20 Function Declarations