Are there any benefits of overloading a method/function to take true_type
or false_type
parameter, compared to using one if
statement?
I see more and more code using overloaded methods with true_type
and false_type
parameters.
A short example using if
statement
void coutResult(bool match)
{
if (match)
cout << "Success." << endl;
else
cout << "Failure." << endl;
}
bool match = my_list.contains(my_value);
coutResult(match);
Compared to using an overloaded function:
void coutResult(true_type)
{
cout << "Success." << endl;
}
void coutResult(false_type)
{
cout << "Failure." << endl;
}
bool match = my_list.contains(my_value);
coutResult(match);
Your second example code wouldn't compile, which is a symptom of the difference between compile-time overload resolution and run-time conditional branching to "choose" which code to execute.
true_type
or false_type
parameter" allows to make this choice at compile time if the decision only depends on types and compile-time constants.if
check" is necessary if the choice can't be done until run time when some variable values are known.In your example the bool match = my_list.contains(my_value)
is obviously not known before running the program, so you can't use overloading.
But the difference is most important for templates, where the choice is not only "which path to execute" but "which code to instantiate and compile and then call". The code from your linked video is rather in this spirit:
Consider this (wrong) code (omitting #include
s and std::
s for brevity):
template<typename InIt>
typename iterator_traits<InIt>::difference_type
distance(InIt first, InIt last)
{
// Make code shorter
typedef typename iterator_traits<InIt>::difference_type Diff;
typedef typename iterator_traits<InIt>::iterator_category Tag;
// Choice
if (is_same<Tag, random_access_iterator_tag>::value)
{
return last - first;
}
else
{
Diff n = 0;
while (first != last) {
++first;
++n;
}
return n;
}
}
There are at least two problems here:
std::list<T>::iterator
), it will actually fail to compile (with an error pointing the line return last - first;
). The compiler has to instantiate and compile the entire function body, including both the if
and the else
branches (even though only one is to be executed), and the expression last - first
is invalid for non-RA iterators.To fix it you can do:
// (assume needed declarations...)
template<typename InIt>
typename iterator_traits<InIt>::difference_type
distance(InIt first, InIt last)
{
// Make code shorter
typedef typename iterator_traits<InIt>::iterator_category Tag;
// Choice
return distanceImpl(first, last, is_same<Tag, random_access_iterator_tag>());
}
template<typename InIt>
typename iterator_traits<InIt>::difference_type
distanceImpl(InIt first, InIt last, true_type)
{
return last - first;
}
template<typename InIt>
typename iterator_traits<InIt>::difference_type
distanceImpl(InIt first, InIt last, false_type)
{
// Make code shorter
typedef typename iterator_traits<InIt>::difference_type Diff;
Diff n = 0;
while (first != last) {
++first;
++n;
}
return n;
}
or alternatively (possible here) with types directly:
/* snip */
distance(InIt first, InIt last)
{
/* snip */
return distanceImpl(first, last, Tag());
}
/* snip */
distanceImpl(InIt first, InIt last, random_access_iterator_tag)
{
return last - first;
}
/* snip */
distanceImpl(InIt first, InIt last, input_iterator_tag)
{
/* snip */
Diff n = 0;
/* snip */
return n;
}
Now only the "correct" distanceImpl
will be instantiated and called (the choice being done at compile time).
This works because the types (e.g. InIt
or Tag
) are known at compile-time, and is_same<Tag, random_access_iterator_tag>::value
is a constant that is known at compile-time too. The compiler can resolve which overload is to be called, only based on types (that's overload resolution).
Note: Even though the "tags" are passed by value, they are only used as unnamed, unused parameters for "dispatch" (their value is not used, only their type) and the compiler can optimize them out.
You can also read Item 47: Use traits classes for information about types from Scott Meyers' Effective C++, Third Edition.
Such overloading is useful for compile-time optimizations. Note that in the example you are referring to a typedef
is used to define a type which matches std::true_type
or std::false_type
. This type is evaluated in compile time. Creating a value of the type in a subsequent function call is necessary just to call the function: you cannot call a function with a type as an argument.
You cannot perform overloading based on a value of a variable. Overloading is performed based on type.
The code
bool match = my_list.contains(my_value);
coutResult(match);
doesn't compile as there is no coutResult(bool)
function:
error: no matching function for call to ‘coutResult(bool)’
note: candidates are: void coutResult(std::true_type)
note: void coutResult(std::false_type)
So, if your expression can be evaluated in compile-time, you may benefit from overloading functions for true_type
and false_type
thus removing additional checks in run-time. But if the expression is not a constant, you must use if
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With