I find it irritating that in standard C++ I can't do std::max(a, b) = x
when it's possible and that it can't handle more than 2 arguments. For the second concern I found this std::min
tutorial and it looks interesting but the usage of pointers confuses me because I don't understand why they are needed here. Since I want to learn more about c++ I wanted to try to create my own version of std::max
while making use of the latest c++ features that I know of.
First I want to make use of concepts to let the user know when the types of arguments are invalid at compilation time:
namespace std1 {
// Helper concept for static_assert
template<typename>
concept False = false;
template<typename T>
concept Boolean = std::is_same_v<T, bool>;
template<typename T>
concept TypeLessThanComparable = requires(T a, T b) {
{ a < b } -> Boolean;
};
template<typename T>
concept TypeLessThanEqComparable = requires(T a, T b) {
{ a <= b } -> Boolean;
};
template<typename T>
concept TypeGreaterThanComparable = requires(T a, T b) {
{ a > b } -> Boolean;
};
template<typename T>
concept TypeGreaterThanEqComparable = requires(T a, T b) {
{ a >= b } -> Boolean;
};
These are the functions themselves:
template<typename T>
constexpr decltype(auto) max(T&& a) noexcept
{
return std::forward<T>(a);
}
template<typename T>
constexpr decltype(auto) max(T&& a, T&& b) noexcept
{
if constexpr(TypeLessThanComparable<T>) {
return a < b ? std::forward<T>(b) : std::forward<T>(a);
}
else if constexpr(TypeLessThanEqComparable<T>) {
return a <= b ? std::forward<T>(b) : std::forward<T>(a);
}
else if constexpr(TypeGreaterThanComparable<T>){
return a > b ? std::forward<T>(a) : std::forward<T>(b);
}
else if constexpr(TypeGreaterThanEqComparable<T>) {
return a >= b ? std::forward<T>(a) : std::forward<T>(b);
}
else {
// if I just put false in static_assert it gives a compilation error no matter what
static_assert(False<void>, "You called max with invalid arguments, cannot find comparison operators for their type");
}
}
template<typename T, typename...Ts>
constexpr decltype(auto) max(T&& a, T&& b, T&& c, Ts&&...d) noexcept
{
return max(a, max(b, max(c, d...)));
}
} // namespace std1
And some tests:
struct A{};
struct B
{
bool operator<(B const&) const noexcept = delete;
bool operator<=(B const&) const noexcept = delete;
bool operator>(B const&) const noexcept = delete;
bool operator>=(B const&) const noexcept;
};
int main()
{
static_assert(std1::max(1, 2) == 2);
int a = 1;
int b = 5;
int c = 3;
int d = 2;
assert(std1::max(a, b, c, d) == b);
std1::max(b, c, d) = 4;
assert(b == 4);
// This gives a compilation error because the static assertion failed
// (void)std1::max(A{}, A{});
// This works
std1::max(B{}, B{}, B{}, B{}, B{});
}
I want to know if the code is well written and could replace std::max
in c++20, maybe it has bugs that I'm not aware of since I am inexperienced in c++. You could also check the compiler explorer link: https://godbolt.org/z/5urgqR.
1 Answer 1
I think this function is unnecessary.
We can deal with defective classes (that don't properly implement the standard LessThanComparable
concept) by either fixing them (preferable) or by providing a comparator argument to std::max
:
auto const b_lessthan = [](const B& a, const B& b){ return !(a>=b); };
std::max({B{}, B{}, B{}, B{}, B{}}, b_lessthan);
Sure, you could make a generic adapter using the same if constexpr
chain as in this code, but are the defective types really that common?
We can arrange for std::max()
to return an lvalue by passing it an initialiser list of std::reference_wrapper
for its arguments:
template<typename... T>
constexpr auto& ref_max(T... args)
{
return std::max({std::ref(args)...,}).get();
}
We now have
#include <algorithm>
#include <functional>
#include <cassert>
int main()
{
static_assert(std::max(1, 2) == 2);
int a = 1;
int b = 5;
int c = 3;
int d = 2;
assert(std::max({a, b, c, d}) == b);
ref_max(b, c, d) = 4;
assert(b == 4);
// This gives a compilation error because the static assertion failed
// (void)std1::max(A{}, A{});
// This works
auto const b_lessthan = [](const B& a, const B& b){ return !(a>=b); };
std::max({B{}, B{}, B{}, B{}, B{}}, b_lessthan);
}
Which isn't so very different than the main()
in the question.
-
\$\begingroup\$ You are right. Only now did I realize I should have added the possibility to provide a custom comparator so that the function can be useful. What I would add is an extra constraint, for example
Callable<T, F>
, so that the comparator is actually valid so that the compiler won't give cryptic error messages. \$\endgroup\$Alexandru Ica– Alexandru Ica2019年06月13日 16:33:41 +00:00Commented Jun 13, 2019 at 16:33 -
\$\begingroup\$ Or do what
std::max()
does, and pass an iterator list and a comparator as the two arguments (but I'm not quite sure whether we can combine that with the fold expression for reference-wrapping). \$\endgroup\$Toby Speight– Toby Speight2019年06月13日 16:37:19 +00:00Commented Jun 13, 2019 at 16:37
constexpr T std::max( std::initializer_list<T> ilist );
? \$\endgroup\$std::max({a, b, c}) = 4
? It also doesn't pass my test withB{}
when not all comparison operators are implemented. \$\endgroup\$lvalue
return type. Thanks for the clarification. There might be a way to usestd::reference_wrapper
to get that behaviour; as for types that don't implement<
- I'd call that a bug in the type (but it's easy to pass a custom comparator). I'll write this in an answer. \$\endgroup\$