6
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ What's wrong with constexpr T std::max( std::initializer_list<T> ilist );? \$\endgroup\$ – Toby Speight Jun 13 at 15:54
  • \$\begingroup\$ @TobySpeight Can it do std::max({a, b, c}) = 4? It also doesn't pass my test with B{} when not all comparison operators are implemented. \$\endgroup\$ – Alexandru Ica Jun 13 at 16:01
  • \$\begingroup\$ Ah, I missed that you wanted an lvalue return type. Thanks for the clarification. There might be a way to use std::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\$ – Toby Speight Jun 13 at 16:06
6
\$\begingroup\$

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.

\$\endgroup\$
  • \$\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 Jun 13 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 Jun 13 at 16:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.