Implementing variadic min / max functions
There is a solution in C++17 which beats all answers proposed so far:
template <typename Head0, typename Head1, typename... Tail>
constexpr auto min(Head0 &&head0, Head1 &&head1, Tail &&... tail)
{
if constexpr (sizeof...(tail) == 0) {
return head0 < head1 ? head0 : head1;
}
else {
return min(min(head0, head1), tail...);
}
}
Notice how this:
- requires only one function
- you can't call this with fewer than two parameters
- it compiles optimally
Using gcc
10.2 with -O3
, the accepted answer compiles to:
min(int, int, int):
cmp esi, edi
jge .L2
cmp esi, edx
mov eax, edx
cmovle eax, esi
ret
.L2:
cmp edi, edx
mov eax, edx
cmovle eax, edi
ret
There are more instructions and a conditional jump for whatever reason. My solution compiles only to:
min(int, int, int):
cmp esi, edx
mov eax, edi
cmovg esi, edx
cmp esi, edi
cmovle eax, esi
ret
This is identical to just calling std::min
recursively for three parameters.
(see https://godbolt.org/z/snavK5)
I appreciate the thought Yakk put into return types so I wouldn't have to, but it gets a lot simpler:
template<typename T>
T&& vmin(T&& val)
{
return std::forward<T>(val);
}
template<typename T0, typename T1, typename... Ts>
auto vmin(T0&& val1, T1&& val2, Ts&&... vs)
{
return (val1 < val2) ?
vmin(val1, std::forward<Ts>(vs)...) :
vmin(val2, std::forward<Ts>(vs)...);
}
Return type deduction is pretty awesome (may require C++14).
live example
This does perfect forwarding on arguments. It relies on RVO for return values, as it returns a value type regardless of the input types, because common_type
does that.
I implemented common_type
deduction, allowing mixed types to be passed in, and the "expected" result type output.
We support the min of 1 element, because it makes the code slicker.
#include <utility>
#include <type_traits>
template<typename T>
T vmin(T&&t)
{
return std::forward<T>(t);
}
template<typename T0, typename T1, typename... Ts>
typename std::common_type<
T0, T1, Ts...
>::type vmin(T0&& val1, T1&& val2, Ts&&... vs)
{
if (val2 < val1)
return vmin(val2, std::forward<Ts>(vs)...);
else
return vmin(val1, std::forward<Ts>(vs)...);
}
int main()
{
std::cout << vmin(3, 2, 0.9, 2, 5) << std::endl;
std::cout << vmin(3., 1.2, 1.3, 2., 5.2) << std::endl;
return 0;
}
Now, while the above is a perfectly acceptable solution, it isn't ideal.
The expression ((a<b)?a:b) = 7
is legal C++, but vmin( a, b ) = 7
is not, because std::common_type
decay
s is arguments blindly (caused by what I consider an over-reaction to it returning rvalue references when fed two value-types in an older implementation of std::common_type
).
Simply using decltype( true?a:b )
is tempting, but it both results in the rvalue reference problem, and does not support common_type
specializations (as an example, std::chrono
). So we both want to use common_type
and do not want to use it.
Secondly, writing a min
function that doesn't support unrelated pointers and does not let the user change the comparison function seems wrong.
So what follows is a more complex version of the above. live example:
#include <iostream>
#include <utility>
#include <type_traits>
namespace my_min {
// a common_type that when fed lvalue references all of the same type, returns an lvalue reference all of the same type
// however, it is smart enough to also understand common_type specializations. This works around a quirk
// in the standard, where (true?x:y) is an lvalue reference, while common_type< X, Y >::type is not.
template<typename... Ts>
struct my_common_type;
template<typename T>
struct my_common_type<T>{typedef T type;};
template<typename T0, typename T1, typename... Ts>
struct my_common_type<T0, T1, Ts...> {
typedef typename std::common_type<T0, T1>::type std_type;
// if the types are the same, don't change them, unlike what common_type does:
typedef typename std::conditional< std::is_same< T0, T1 >::value,
T0,
std_type >::type working_type;
// Careful! We do NOT want to return an rvalue reference. Just return T:
typedef typename std::conditional<
std::is_rvalue_reference< working_type >::value,
typename std::decay< working_type >::type,
working_type
>::type common_type_for_first_two;
// TODO: what about Base& and Derived&? Returning a Base& might be the right thing to do.
// on the other hand, that encourages silent slicing. So maybe not.
typedef typename my_common_type< common_type_for_first_two, Ts... >::type type;
};
template<typename... Ts>
using my_common_type_t = typename my_common_type<Ts...>::type;
// not that this returns a value type if t is an rvalue:
template<typename Picker, typename T>
T pick(Picker&& /*unused*/, T&&t)
{
return std::forward<T>(t);
}
// slight optimization would be to make Picker be forward-called at the actual 2-arg case, but I don't care:
template<typename Picker, typename T0, typename T1, typename... Ts>
my_common_type_t< T0, T1, Ts...> pick(Picker&& picker, T0&& val1, T1&& val2, Ts&&... vs)
{
// if picker doesn't prefer 2 over 1, use 1 -- stability!
if (picker(val2, val1))
return pick(std::forward<Picker>(pick), val2, std::forward<Ts>(vs)...);
else
return pick(std::forward<Picker>(pick), val1, std::forward<Ts>(vs)...);
}
// possibly replace with less<void> in C++1y?
struct lesser {
template<typename LHS, typename RHS>
bool operator()( LHS&& lhs, RHS&& rhs ) const {
return std::less< typename std::decay<my_common_type_t<LHS, RHS>>::type >()(
std::forward<LHS>(lhs), std::forward<RHS>(rhs)
);
}
};
// simply forward to the picked_min function with a smart less than functor
// note that we support unrelated pointers!
template<typename... Ts>
auto min( Ts&&... ts )->decltype( pick( lesser(), std::declval<Ts>()... ) )
{
return pick( lesser(), std::forward<Ts>(ts)... );
}
}
int main()
{
int x = 7;
int y = 3;
int z = -1;
my_min::min(x, y, z) = 2;
std::cout << x << "," << y << "," << z << "\n";
std::cout << my_min::min(3, 2, 0.9, 2, 5) << std::endl;
std::cout << my_min::min(3., 1.2, 1.3, 2., 5.2) << std::endl;
return 0;
}
The downside to the above implementation is that most classes do not support operator=(T const&)&&=delete
-- ie, they do not block rvalues from being assigned to, which can lead to surprises if one of the types in the min
does not . Fundamental types do.
Which is a side note: start deleting your rvalue reference operator=
s people.