C++ Universal Reference and Perfect Forwarding

Introduction

Suppose we have $m$ classes and each class has $n$ constructors which consumes one single parameter, without using template (due to there is no universal way for implementation), how many constructors should we implement in total? The answer is obvious. We need to implement $mn$ constructors in total. The complexity is quadratic and this is normal in modern programming.

An example of the implementation for class A which contains 2 constructors could be

1
2
3
4
struct A {
A(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
A(int& n) { std::cout << "lvalue overload, n=" << n << "\n"; }
};

Suppose we have a new class which has $m$ member variables, each of the member variables is a class of the $m$ classes we have declared above, if we want to implement every combination of the possible constructors, without using template, at least how many constructors should we implement in total? The answer is not difficult either. We need to implement $n^m$ constructors in total. The complexity becomes exponential and when $m$ is large we will just not affording implementing.

An example of the implementation for class B which contains 3 member variables could be

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class B {
public:
B(int&& t1, int&& t2, int&& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
B(int& t1, int&& t2, int&& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
B(int&& t1, int& t2, int&& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
B(int&& t1, int&& t2, int& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
B(int& t1, int& t2, int&& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
B(int& t1, int&& t2, int& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
B(int&& t1, int& t2, int& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
B(int& t1, int& t2, int& t3) :
a1_{t1},
a2_{t2},
a3_{t3}
{
}
private:
A a1_, a2_, a3_;
};

There are $2^3 = 8$ implementations for the constructors in total and the implementation for this class already looks very “heavy”.

These are combinatorial implementation could be extremely simplified by universal reference and perfect forwarding.

Universal Reference and Perfect Forwarding

Universal Reference

The “universal reference”, proposed by Scott Meyers, alleviated us from this combinatorial mathematics.

Instead of creating $n^m$ constructors, we would only create $m$ constructors using universal reference!

An example of the equivalent implementation for class B could be

1
2
3
4
5
6
7
8
9
10
11
12
13
class B {
public:
template<class T1, class T2, class T3>
B(T1&& t1, T2&& t2, T3&& t3) :
a1_{std::forward<T1>(t1)},
a2_{std::forward<T2>(t2)},
a3_{std::forward<T3>(t3)}
{
}

private:
A a1_, a2_, a3_;
};

I have also touched some bases of the universal reference rules in my previous blog post “C++ Reference Collapsing”.

Perfect Forwarding

Suppose we want to create a wrapper function which returns a std::unique_ptr<T> for every single possible class T. Such wrapper function could just be the C++ standard library function std::make_unique.

Because there could be infinitely number of classes and the number of arguments for the functions could all be different, even with the universal reference mentioned above, it is impossible to cover all the use cases.

For example, we will have to implement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<class T, class U1>
std::unique_ptr<T> make_unique1(U1&& u1)
{
return std::unique_ptr<T>(new T(std::forward<U1>(u1)));
}

template<class T, class U1, class U2>
std::unique_ptr<T> make_unique2(U1&& u1, U2&& u2)
{
return std::unique_ptr<T>(new T(std::forward<U1>(u1), std::forward<U2>(u2)));
}

template<class T, class U1, class U2, class U3>
std::unique_ptr<T> make_unique3(U1&& u1, U2&& u2, U3&& u3)
{
return std::unique_ptr<T>(new T(std::forward<U1>(u1), std::forward<U2>(u2), std::forward<U3>(u3)));
}

...

This will go endless.

Fortunately, perfect forwarding resolves this issue. Instead of implementing infinite number of wrapper functions, we only need to create one.

1
2
3
4
5
template<class T, class... U>
std::unique_ptr<T> make_unique(U&&... u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}

Example

A full example is copied from the std::forward webpage on CPP Reference.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>
#include <memory>
#include <utility>

struct A {
A(int&& n) { std::cout << "rvalue overload, n=" << n << "\n"; }
A(int& n) { std::cout << "lvalue overload, n=" << n << "\n"; }
};

class B {
public:
template<class T1, class T2, class T3>
B(T1&& t1, T2&& t2, T3&& t3) :
a1_{std::forward<T1>(t1)},
a2_{std::forward<T2>(t2)},
a3_{std::forward<T3>(t3)}
{
}

private:
A a1_, a2_, a3_;
};

template<class T, class U>
std::unique_ptr<T> make_unique1(U&& u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)));
}

template<class T, class... U>
std::unique_ptr<T> make_unique(U&&... u)
{
return std::unique_ptr<T>(new T(std::forward<U>(u)...));
}

int main()
{
auto p1 = make_unique1<A>(2); // rvalue
int i = 1;
auto p2 = make_unique1<A>(i); // lvalue

std::cout << "B\n";
auto t = make_unique<B>(2, i, 3);
}

References

C++ Universal Reference and Perfect Forwarding

https://leimao.github.io/blog/Universal-Reference-Perfect-Forwarding/

Author

Lei Mao

Posted on

07-15-2022

Updated on

07-15-2022

Licensed under


Comments