[C++11] Universal reference and perfect forwarding

Article directory

  • 1. & amp; & amp; in the template-universal reference
  • 2. Perfect forwarding and its application scenarios
  • 3. Code used
    • 3.1 string.h
    • 3.2 list.h
    • 3.3 test.cpp

1. & amp; & amp;-universal reference in templates

First let’s look at this piece of code:


There are 4 functions here. We can easily see that they have an overloaded relationship.
Then we give a function template like this

Let’s take a look at the parameters of this function template, T & amp; & amp; t

There are two & amp; & amp; here, so is it an rvalue reference?

No!
& amp; & amp; in the template does not represent an rvalue reference, but a universal reference, which can receive both lvalues and rvalues.
When we instantiate this function template

You can pass lvalues or rvalues.
What we pass is an lvalue, so the parameter t is an lvalue reference. What we pass is an rvalue, and the parameter t is an rvalue reference.
Therefore, it is also called reference folding in some places, that is, when we pass an lvalue, it seems to fold & amp; & amp;.
However, you can see that after we receive t here, it is passed down one layer

Then everyone needs to think about how t is passed down to Fun inside the PerfectForward function. What will t match if it is passed to Fun?

Then let’s run the program and see the results:


Eh! what’s up?
Why are all the matching lvalue references?

So why is this happening here?

Do you still remember that I mentioned something to you in the previous article:

That is,an rvalue cannot take an address, but giving an alias to an rvalue will cause the rvalue to be stored in a specific location, and the address of that location can be obtained.
For example: the address of literal 10 cannot be taken, but after rr1 is referenced, the address of rr1 can be taken.
address, you can also modify rr1.

If it can take addresses and assign values, wouldn’t it become an lvalue?
Therefore, After an rvalue is referenced by an rvalue, the attribute will become an lvalue
If you think about it, this design is actually reasonable:
For example, this scene

Transferring a resource can also be considered as modifying it, while the dying value of a temporary variable or anonymous object cannot be modified.

Then if we look back at our above questions, everyone should understand


Even if what we pass is an rvalue, it will become an lvalue after being referenced by an rvalue, so if it is passed to Fun, it will match an lvalue.

so:

The universal reference of the template only provides the ability to receive lvalues and rvalues at the same time. The function is to limit the types received, but it degenerates into lvalues in subsequent use.
But in some scenarios, we hope to maintain its lvalue or rvalue attributes during the transfer process, so how to do this?
You need to use the Perfect Forwarding
we will learn below.

2. Perfect forwarding and its application scenarios

First, let’s look at a corresponding scenario:

We have simulated and implemented lists before, so make a copy

Some things that are no longer needed will be deleted.
In conjunction with the string we created ourselves, I will also comment out the move copy and move structures we added to the string.
I write a piece of code like this

Then our previous implementation did not have move semantics, so

All are deep copies, regardless of lvalue or rvalue.
Then we release the move construction and move copy of string, and then I add the rvalue reference version to push_back of list.

But our push_back reuses insert, so insert also adds an rvalue reference version.

Run again

Hey, why doesn’t it work? The push_back of the list is still a deep copy

What’s the problem?

, is it the problem we mentioned above?
An rvalue becomes an lvalue when it is referenced by an rvalue.
If the parameter passed to push_back for the first time is an rvalue, the rvalue reference version of push_back is called. However, when insert is called in push_back and passed for the second time, it becomes an lvalue.

So in the end, whether it is rvalue or lvalue push_back, the final inserted insert is the lvalue version, so it is a deep copy

So how to solve this problem?

How to maintain its lvalue or rvalue attributes during the transfer process?
This requires the use of Perfect forwarding
std::forward perfect forwarding retains the object’s native type attributes during the parameter transfer process

It is also a function template provided in the library.
Then we call forward directly to maintain the native attributes of the parameters.

Then let’s run it again

Oops, why not?
, because there is actually another layer of transmission below

When creating a new node in insert, the constructor of node must be called.

So we also need to add forward here.
but!

We only have the lvalue reference version of node’s constructor
So, we need to add another version of rvalue reference

Moreover, when initializing _data here, we also need to use forward to maintain the attributes of
Then this time

That’s it, push_back of rvalue is a mobile copy

Then we have the perfect forwarding of our initial scene:


All match the version referenced by rvalue
what to do?
Just add a perfect forwarding

3. Code used

3.1 string.h

#pragma once

namespace bit
{<!-- -->
class string
{<!-- -->
public:
typedef char* iterator;
iterator begin()
{<!-- -->
return _str;
}
iterator end()
{<!-- -->
return _str + _size;
}

string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{<!-- -->
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string & s)
{<!-- -->
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// copy construction
string(const string & s)
:_str(nullptr)
{<!-- -->
cout << "string(const string & amp; s) -- deep copy" << endl;
string tmp(s._str);
swap(tmp);
}
// move constructor
string(string & amp; & amp; s)
:_str(nullptr)
{<!-- -->
cout << "string(string & amp; & amp; s) -- move copy" << endl;
swap(s);
}
// Assignment overloading
string & amp; operator=(const string & amp; s)
{<!-- -->
cout << "string & amp; operator=(string s) -- deep copy" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// move assignment
string & amp; operator=(string & amp; & amp; s)
{<!-- -->
cout << "string & amp; operator=(string & amp; & amp; s) -- move assignment" << endl;
swap(s);
return *this;
}
~string()
{<!-- -->
delete[] _str;
_str = nullptr;
}
char & operator[](size_t pos)
{<!-- -->
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{<!-- -->
if (n > _capacity)
{<!-- -->
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{<!-- -->
if (_size >= _capacity)
{<!-- -->
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
+ + _size;
_str[_size] = '\0';
}
//string operator + =(char ch)
string & operator + =(char ch)
{<!-- -->
push_back(ch);
return *this;
}
string operator + (char ch)
{<!-- -->
string tmp(*this);
tmp + = ch;
return tmp;
}
const char* c_str() const
{<!-- -->
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // Does not include the last marked \0
};
string to_string(int value)
{<!-- -->
bool flag = true;
if (value < 0)
{<!-- -->
flag = false;
value = 0 - value;
}
string str;
while (value > 0)
{<!-- -->
int x = value % 10;
value /= 10;
str + = ('0' + x);
}
if (flag == false)
{<!-- -->
str + = '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}

3.2 list.h

#pragma once
#include<assert.h>

namespace yin
{<!-- -->
template<class T>
struct list_node
{<!-- -->
list_node<T>* _next;
list_node<T>* _prev;
T_data;

list_node(const T & amp; x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(x)
{<!-- -->}

list_node(T & amp; & amp; x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(forward<T>(x))
{<!-- -->}
};


// 1. The iterator is either a native pointer
// 2. Iterators are either encapsulation of native pointers by custom types, simulating the behavior of pointers.
template<class T, class Ref, class Ptr>
struct __list_iterator
{<!-- -->
typedef list_node<T> node;
typedef __list_iterator<T, Ref, Ptr> self;
node* _node;

__list_iterator(node* n)
:_node(n)
{<!-- -->}

Ref operator*()
{<!-- -->
return _node->_data;
}

Ptr operator->()
{<!-- -->
return &_node->_data;
}

self & operator + + ()
{<!-- -->
_node = _node->_next;

return *this;
}

self operator + + (int)
{<!-- -->
self tmp(*this);
_node = _node->_next;

return tmp;
}

self & operator--()
{<!-- -->
_node = _node->_prev;

return *this;
}

self operator--(int)
{<!-- -->
self tmp(*this);
_node = _node->_prev;

return tmp;
}

bool operator!=(const self & amp; s)
{<!-- -->
return _node != s._node;
}

bool operator==(const self & s)
{<!-- -->
return _node == s._node;
}
};


template<class T>
class list
{<!-- -->
typedef list_node<T> node;
public:
typedef __list_iterator<T, T & amp;, T*> iterator;
typedef __list_iterator<T, const T & amp;, const T*> const_iterator;

iterator begin()
{<!-- -->
//iterator it(_head->_next);
//return it;
return iterator(_head->_next);
}

const_iterator begin() const
{<!-- -->
return const_iterator(_head->_next);
}

iterator end()
{<!-- -->
return iterator(_head);
}

const_iterator end() const
{<!-- -->
//iterator it(_head->_next);
//return it;
return const_iterator(_head);
}

void empty_init()
{<!-- -->
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
}

list()
{<!-- -->
empty_init();
}

template <class Iterator>
list(Iterator first, Iterator last)
{<!-- -->
empty_init();

while (first != last)
{<!-- -->
push_back(*first);
+ + first;
}
}

void swap(list<T> & amp; tmp)
{<!-- -->
std::swap(_head, tmp._head);
}

list(const list<T> & lt)
{<!-- -->
empty_init();

list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}

// lt1 = lt3
list<T> & operator=(list<T> lt)
{<!-- -->
swap(lt);
return *this;
}

~list()
{<!-- -->
clear();
delete _head;
_head = nullptr;
}

void clear()
{<!-- -->
iterator it = begin();
while (it != end())
{<!-- -->
//it = erase(it);
erase(it + + );
}
}

void push_back(const T & x)
{<!-- -->
insert(end(), x);
}

void push_back(T & amp; & amp; x)
{<!-- -->
insert(end(), forward<T>(x));
}

void push_front(const T & x)
{<!-- -->
insert(begin(), x);
}

void pop_back()
{<!-- -->
erase(--end());
}

void pop_front()
{<!-- -->
erase(begin());
}

void insert(iterator pos, const T & amp; x)
{<!-- -->
node* cur = pos._node;
node* prev = cur->_prev;

node* new_node = new node(x);

prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}

void insert(iterator pos, T & amp; & amp; x)
{<!-- -->
node* cur = pos._node;
node* prev = cur->_prev;

node* new_node = new node(forward<T>(x));

prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}

iterator erase(iterator pos)
{<!-- -->
assert(pos != end());

node* prev = pos._node->_prev;
node* next = pos._node->_next;

prev->_next = next;
next->_prev = prev;
delete pos._node;

return iterator(next);
}
private:
node* _head;
};
}

3.3 test.cpp

void Fun(int & amp; x) {<!-- --> cout << "lvalue reference" << endl; }
void Fun(const int & amp; x) {<!-- --> cout << "const lvalue reference" << endl; }
void Fun(int & amp; & amp; x) {<!-- --> cout << "rvalue reference" << endl; }
void Fun(const int & amp; & amp; x) {<!-- --> cout << "const rvalue reference" << endl; }

template<typename T>
void PerfectForward(T & amp; & amp; t)
{<!-- -->
Fun(forward<T>(t));
}

int main()
{<!-- -->
PerfectForward(10); // rvalue
int a;
PerfectForward(a); // lvalue
PerfectForward(move(a));// rvalue

const int b = 8;
PerfectForward(b);// const lvalue
PerfectForward(move(b)); // const rvalue

return 0;
}

//#include "list.h"
//
//int main()
//{<!-- -->
// yin::list<bit::string> lt;
//
// bit::string s1("hello world");
//lt.push_back(s1);
//
// lt.push_back(bit::string("hello world"));
// lt.push_back("hello world");
//
// return 0;
//}