|
| 1 | +# Alternatives To Universal References |
| 2 | + |
| 3 | +Overloading a method taking a universal reference can cause problems and |
| 4 | +head-aches, as univeral references bind to anything they can. This means that |
| 5 | +when you overload such a method and wish to pass a parameter to that overload, |
| 6 | +but the parameter does not *quite* fit the declared function parameter, the |
| 7 | +univeral reference overload will hit. |
| 8 | + |
| 9 | +```C++ |
| 10 | +template<typename T> |
| 11 | +void print(T&& argument) |
| 12 | +{ |
| 13 | + std::cout << std::forfward<T>(argument); |
| 14 | +} |
| 15 | + |
| 16 | +void print(int integer) |
| 17 | +{ |
| 18 | + std::cout << "integer: " << argument; |
| 19 | +} |
| 20 | +``` |
| 21 | +
|
| 22 | +Passing a plain integer to the above will work just fine, as overload resolution |
| 23 | +will prefer the more specialized `int` overload to the possible (but not chosen) |
| 24 | +`int&` overload resulting from the universal reference. However, passing a |
| 25 | +`short` will already cause problems. You would expect the `int` overload to be |
| 26 | +called, but because `short&` reference is a better match than `int` for a value |
| 27 | +of type `short`, the universal reference overload will be picked. |
| 28 | +
|
| 29 | +Therefore, it may sometimes be necessary to consider alternatives to overloading |
| 30 | +on universal references. These will be presented below. |
| 31 | +
|
| 32 | +## Not Overloading |
| 33 | +
|
| 34 | +The simplest alternative to overloading on universal references is to not |
| 35 | +overload at all, but rename the functions with more specialized names. For |
| 36 | +example: |
| 37 | +
|
| 38 | +```C++ |
| 39 | +void print_integer(int integer) { ... } |
| 40 | +``` |
| 41 | + |
| 42 | +Would no longer cause problems with overload resolution. |
| 43 | + |
| 44 | +Advantages: |
| 45 | ++ Solves the problem |
| 46 | ++ More expressive (?) |
| 47 | + |
| 48 | +Disadvantage: |
| 49 | +- Perfect forwarding lost (would have to overload for all variants of `int`, |
| 50 | + `const int&&` etc.) |
| 51 | +- Lose the right to use overloads (Human Rights Charta, paragraph 3) |
| 52 | + |
| 53 | +## `const T&` |
| 54 | + |
| 55 | +If the template parameter can be replaced with a concrete type such as |
| 56 | +`std::string`, you can replace the universal reference with a `const&` to that |
| 57 | +type. Given that a `const&` can bind to lvalues as well as rvalues, passing by |
| 58 | +`const&` will reduce the number of types that function will accept. |
| 59 | + |
| 60 | +Advantages: |
| 61 | ++ Can use overloading |
| 62 | + |
| 63 | +Disadvantages: |
| 64 | +- Lose perfect forwarding |
| 65 | +- (Therefore) not as efficient as universal referenes |
| 66 | + |
| 67 | +## Tag Dispatching |
| 68 | + |
| 69 | +As the saying goes: all problems in software engineering can be solved by an |
| 70 | +extra level of indirection. We can solve the overload resolution problem by |
| 71 | +adding a level of indirection and *dispatching a tag* in dependence of the |
| 72 | +parameter passed to the universal reference method. With a *tag* we mean some |
| 73 | +trait that differentiates one type from one another. The hidden function then |
| 74 | +not only takes the universal reference parameter, but also a tag parameter so |
| 75 | +that compile-time overload resolution will kick-in to select the correct |
| 76 | +overload. You can see this idea of tag-dispatching as a close alternative to |
| 77 | +SFINAE, which is discussed below. |
| 78 | + |
| 79 | +To distinguish between integral and non-integral types for `print`, we can use |
| 80 | +the `std::is_integral` type trait on our parameter. For integral types, the |
| 81 | +specialized version will inherit from `std::true_type`, so the indirected |
| 82 | +overload for integers will take an anonymous `std::true_type` parameter as its |
| 83 | +second argument (the `std::is_integral` type will be sliced to |
| 84 | +`std::true_type`). For non-integral types, `std::is_integral` will inherit from |
| 85 | +`std::false_type`. Therefore, we can write something like this: |
| 86 | + |
| 87 | +```C++ |
| 88 | +template<typename T> |
| 89 | +void print(T&& argument, std::false_type) |
| 90 | +{ |
| 91 | + std::cout << argument; |
| 92 | +} |
| 93 | + |
| 94 | +void print(int integer, std::true_type) |
| 95 | +{ |
| 96 | + std::cout << "integer: " << integer; |
| 97 | +} |
| 98 | + |
| 99 | +template<typename T> |
| 100 | +void print(T&& argument) |
| 101 | +{ |
| 102 | + print(std::forward<T>(argument), std::is_integral<T>{}); |
| 103 | +} |
| 104 | +``` |
| 105 | +
|
| 106 | +## SFINAE |
| 107 | +
|
| 108 | +SFINAE is the go-to solution for restricting the types of parameters template |
| 109 | +functions are allowd to accept, and can naturally also be used here. We'll just |
| 110 | +have to be sure to remove any (lvalue/rvalue) reference components from the |
| 111 | +template type as `int&` is not considered integral (according to |
| 112 | +`std::is_integral`) while `int` is. For this we'll use `std::decay`, which |
| 113 | +removes all reference components as well as cv-qualifications from a template type. |
| 114 | +
|
| 115 | +```C++ |
| 116 | +template<typename T> |
| 117 | +std::enable_if_t<!std::is_same<int, std::decay_t<T>>::value> print(T&& argument) |
| 118 | +{ |
| 119 | + std::cout << argument; |
| 120 | +} |
| 121 | +
|
| 122 | +void print(int integer) |
| 123 | +{ |
| 124 | + std::cout << "integer: " << integer; |
| 125 | +} |
| 126 | +``` |
0 commit comments