Implementing variadic Max function in C++20
Asked Answered
M

2

9

Despite, the fact, we have std::max, I wanted to try if it is possible to make a Max version that takes variadic arguments and calls the Max recursively for finding the max element.

I saw similar posts in stack overflow, but those are old and most of them use std::max inside. Since I have a specific error and using a newer compiler, this post is not duplicated easily.

Following is the code I have written:

#include <iostream>
#include <string>
#include <format>
using namespace std::string_literals;

template <typename T>
constexpr T Max(T&& value)
{  
  return value;
}

template <typename T, typename... Ts>
constexpr T Max(T&& value, Ts&&... args)
{
    const T maxRest = Max(args...);

    return (value > maxRest) ? value : maxRest;
}

int main()
{
    std::cout << std::format("Maximum integer: {}\n", Max(1));
    std::cout << std::format("Maximum integer: {}\n", Max(5, 2, 10, 6, 8));
    std::cout << std::format("Maximum integer: {}\n", Max("string1", "string2"s));  // error in this line!!
    std::cout << std::format("Maximum double: {}\n", Max(3.14, 1.23, 2.56, 0.98));
    return 0;
}

For which I am getting:

main.cc(79, 21) : error C2440 : 'initializing' : cannot convert from 'std::string' to 'const char (&)[8]'
main.cc(79, 21) : message: Reason: cannot convert from 'std::string' to 'const char [8]'
main.cc(79, 21) : message: No user - defined - conversion operator available that can perform this conversion, or the operator cannot be called
main.cc(87, 55) : message: see reference to function template instantiation 'T Max<const char(&)[8],std::string>(T,std::string &&)' being compiled
with
[
    T = const char(&)[8]
]
  • I think the error is coming from the function call: Max("string1", "string2"s));. I do not know, how can resolve this.
  • Likewise, I am also feeling that I am writing more to achieve this Max function in . Does anybody have any suggestion to make the two Max functions into one?
Mimeograph answered 10/7, 2023 at 12:30 Comment(3)
Do you really mean Max("string1", "string2"s) and not Max("string1"s, "string2"s)?Frowsy
@TedLyngmo yes I wanted, std::string,const char* and std::string_view, and may be for all those have less that or greater than operator defined.Mimeograph
I see. You then need to deal with the case where only const char* are supplied.Frowsy
S
13

Likewise, I am also feeling that I am writing more to achieve this Max function in [...] ?

Your Max function can maximize the simplicity by

constexpr auto Max(auto const& value, auto const&... args)
{
    if constexpr (sizeof...(args) == 0u) // Single argument case!
        return value;
    else // For the Ts...
    {
        const auto max = Max(args...);
        return value > max ? value : max;
    }
}

See live demo in godbolt.org

Update: As pointed out in the comment section by @TedLyngmo, the above does not work if you only pass consecutive const char*s (string literals). Ex. scenario

Max("string1"s, "string2", "string4", "string3") // result is "string2" instead of "string4"

Because this is resulting a pointer comparison rather than the comparison you want. It was the case also for your original shown code. You might want to handle this situation separately.

For instance, in the following code example, if the value is convertible to std::string_view, we convert it to std::string_view and do the greater check:

#include <type_traits>  // std::is_convertible

constexpr auto Max(auto const& value, auto const&... args)
{
    if constexpr (sizeof...(args) == 0u) // Single argument case!
    {
        if constexpr (std::is_convertible_v<decltype(value), std::string_view>)
            return std::string_view{value};
        else
            return value;
    }
    else // For the Ts...
    {
        const auto max = Max(args...);
        return value > max ? value: max;
    }
}

See live demo in godbolt.org

Likewise, each time when you use this Max function, always remember to check whether the passed argument is some type of pointer, which is clearly not handled by it.


I think the error is coming from the function call: Max("string1", "string2"s)); . I do not know, how can to resolve this.

When you call Max("string1", "string2"s)), the compiler deduces the T (i.e. return type), to be const char[8], that is the type of the first argument(i.e. "string1"). However, the second argument is a std::string (i.e. "string2"s). Now for the line :

const T maxRest = Max(args...);

this std::string must be now implicitly convertible to const char [8].This is not viable, and hence the compiler produces a type mismatch error.

To fix the issue, you can simply let the compiler deduce the type for you; That means, instead of defining or assuming the the return type will be always T, use auto so that compiler can deduce the type for you.

template <typename T, typename... Ts>
constexpr auto Max(T const& value, Ts const&... args)
//        ^~~~ ---> Simply 'auto'
{
    const auto maxRest = Max(args...);
    //    ^~~~ ---> Simply 'auto'
    return (value > maxRest) ? value : maxRest;
}

See live demo in godbolt.org

Alternatively, you could also use the std::common_type_t for defining the return type.

#include <type_traits> // std::common_type_t

        template <typename T, typename... Ts>
constexpr auto Max(T const& value, Ts const&... args)
-> std::common_type_t<T, Ts...>
{
    // ....
}
Shephard answered 10/7, 2023 at 12:32 Comment(13)
this is still comparing addresses of the string literals, no?Mantelletta
Wow that looks rather simple. Can you give me ref, where I can find those features used?Mimeograph
oh right, I missed the s in "string2"sMantelletta
In addition, you might use std::less/std::greater to handle unrelated pointer comparison.Afroasiatic
I don't think common_type has a specialization for string and const char*.Imputation
It may need some special handling for const char*s. Max("string1"s, "string2", "string4", "string3") gives string3 in gcc and string2 in clang.Frowsy
@Shephard It's those darn pointers :-) Perhaps catching char* and turning them into string_views would be an optionFrowsy
@TedLyngmo Thanks for bringing this up. I forgot that even std::max had the same issue: Why is std::max not working for string literals?. I have updated as per.. :)Shephard
@JeJo, You're right, I forgot and missed that it has the core ternary-based specialization that covers this case., almost like that's the entire point of common_type ;)Imputation
@JeJo: "a" < "b" is UB, whereas std::less<>{}("a", "b") is just implementation specific (compare pointer, not string content).Afroasiatic
You have to use common_type to ensure all types are same. Creating std::initializer_list<common> seems the simpler, and get rid of variadic template for the remaining part.Demo.Afroasiatic
Up to OP to implement Max(initializer_list<T>) ;-)Afroasiatic
Yeap. BTW The shown code also required to take care the case of string literals separately: gcc.godbolt.org/z/oY1cznodzShephard
S
4

Likewise, I am also feeling that I am writing more to achieve this Max function in [...]?

As extension to the other answer, using fold expressions, the Max can be made non-recursive as well.

#include <type_traits> // std::common_type, std::remove_cvref
#include <functional>  // std::greater

template<typename... T>  // common type helper
using CommonType = std::common_type_t<std::remove_cvref_t<T>...>;

constexpr auto Max(auto const& value, auto const&... args)
{
    CommonType<decltype(value), decltype(args)...> maxVal = value;

    return sizeof...(args) == 0u ? maxVal 
        : (((maxVal = std::greater{}(args, maxVal) ? args : maxVal), ...)
            , maxVal);
}

Live demo in godbolt.org


However, for the case of consecutive string literals, that requires some addition:

template<typename... T>  // common type helper
using CommonType = std::common_type_t<std::remove_cvref_t<T>...>;

// For string literals comparison.
constexpr auto handleStrLiterals(auto const& t)
{
    if constexpr (std::is_convertible_v<decltype(t), std::string_view>)
            return std::string_view{ t };
    else    return t;
};

constexpr auto Max(auto const& value, auto const&... args)
{
    CommonType<decltype(handleStrLiterals(value)), decltype(args)...>
        maxVal = handleStrLiterals(value);
    return sizeof...(args) == 0u ? maxVal 
        : (((maxVal = std::greater{}(args, maxVal) ? args : maxVal), ...)
            , maxVal);
}

Live demo in godbolt.org

Shephard answered 10/7, 2023 at 20:55 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.