MyClass
MyClass

Reputation: 362

Implementing variadic Max function in C++20

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]
]

Upvotes: 9

Views: 476

Answers (2)

JeJo
JeJo

Reputation: 32952

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

Upvotes: 4

JeJo
JeJo

Reputation: 32952

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...>
{
    // ....
}

Upvotes: 13

Related Questions