25

In C++03 the following code works fine:

int main()
{
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);

    std::vector<int> v2;
    v2.push_back(2);
    v2.push_back(3);
    v2.push_back(4);

    std::transform(v.begin(), v.end(), v2.begin(), v2.begin(), std::max<int>);
    return 0;
}

In C++11 this doesn't work because it added an overload for std::max that contains an initializer_list. Therefore, you have to use a very ugly cast to choose the correct overload:

static_cast<const int& (*)(const int&, const int&)>(std::max)

I have a few questions.

  • Why did the standard committee decide to do this knowing it would (probably) break existing code and force the user to create an ugly cast?
  • Are future standards of C++ going to attempt to alleviate this problem?
  • What is a workaround?
user4112979
  • 312
  • 3
  • 11
  • You may still use lambda instead of the cast. – Jarod42 Oct 06 '14 at 11:44
  • I'm not sure if you are supposed to explicitly list the type arguments to template functions like `std::max` and use them as functors just because they are in STL and it is allowed by syntax – Piotr Skotnicki Oct 06 '14 at 11:48
  • 1
    @PiotrS. : The C++ standard does have footnotes, which cover the "you're [not] supposed to" angle. It's not a mere specification. But there's no note on this particular usage. Nor is there a claim that the footnotes are a complete set of suggestions. – MSalters Oct 06 '14 at 11:53
  • 2
    The answer to bullet one: *"Seemed like a good idea at the time."* :) I wouldn't be surprised if it turns out that nobody in the committee saw this coming. – jrok Oct 06 '14 at 12:04
  • Not an answer because I can't check it at this time, but isn't the implementation allowed to add default arguments and/or overloads anyway? That would make the code already non-compliant by C++03 standards, even if it usually worked. – MSalters Oct 06 '14 at 12:26
  • 2
    @MSalters Extra default arguments aren't allowed for global or non-member functions. Extra signatures are allowed though. – T.C. Oct 06 '14 at 12:41
  • @T.C. So `std::max(T a, T b, T c)` was already legal and therefore the code above already was flawed in C++03? Makes sense. – MSalters Oct 06 '14 at 12:45
  • 1
    @MSalters Technically, `T a, T b, T c` probably wasn't legal - it breaks in the pathological case when `T` is also used as the comparator. Four `T`s though might be. – T.C. Oct 06 '14 at 12:50
  • I can think of at least one more function template that will no longer work if in C++03 one tried to explicitly specify the types rather than allow compiler deduce them, which is `std::make_pair`. It previously took arguments by value, so that `make_pair` resulted in `make_pair(int, int)`, where as in C++11 it takes forwarding references, which results in `make_pair(int&&, int&&)`. So the point I'm trying to make is that one should not specify the types explicitly for `std` template functions. Another thing is that there can be non-templated overloads that already take e.g. `int`. – Piotr Skotnicki Oct 06 '14 at 12:51
  • The "*max as predicate fails in C++11*" issue has been presented in [GoingNative 2013: Don't Help the Compiler](http://channel9.msdn.com/Events/GoingNative/2013/Don-t-Help-the-Compiler) @ 0:50:47, with a rule: *"don't use explicit template arguments"* – Piotr Skotnicki Oct 07 '14 at 14:47

5 Answers5

12

If you are doing this sufficiently frequently, you might want to write a transparent functor wrapper:

struct my_max {
    template<class T>
    const T& operator()(const T& a, const T& b) const{
        return std::max(a, b);
    }
};

Then you can simply do

std::transform(v.begin(), v.end(), v2.begin(), v2.begin(), my_max());

whenever you need it, rather than writing a lambda or a cast each time. This is basically the same idea as the transparent operator functors - let the template arguments be deduced at the actual call site rather than explicitly specified when you create the functor.

If you want to make this fancier, you can even have operator() take heterogeneous types and add perfect forwarding and use trailing return types:

struct my_max {
    template<class T, class U>
    constexpr auto operator()( T&& t, U&& u ) const
      -> decltype(t < u ? std::forward<U>(u) : std::forward<T>(t)){
        return t < u ? std::forward<U>(u) : std::forward<T>(t);
    }
};

In C++14, this is simplified to

struct my_max {
    template<class T, class U>
    constexpr decltype(auto) operator()( T&& t, U&& u ) const{
        return t < u ? std::forward<U>(u) : std::forward<T>(t);
    }
};
T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Why don't you just use two separate template parameters and trailing return types? – Columbo Oct 06 '14 at 12:03
  • 1
    What I'm hoping to see is a transparent comparator syntax like `std::less<>()`, i.e. `std::max<>()`. – user4112979 Oct 06 '14 at 12:06
  • 1
    @user4112979 Well, `std::less` is a class template. `std::max` is a function template. – T.C. Oct 06 '14 at 12:09
  • 2
    Be warned that the forwarding version will break horribly in the (somewhat pathological) case when `T` or `U` is a type with a "stealing" move ctor, either `t` or `u` is an r-value reference and `<` takes the appropriate operand by value. – Angew is no longer proud of SO Oct 06 '14 at 12:26
  • @Angew Good point, I guess I'll remove the forward for the `<` part, though it might break in some other pathological case instead. Oh well. – T.C. Oct 06 '14 at 12:30
  • A `forward` should only happen at the **last** use of a perfectly forwarded variable as a rule. Oh, and think about for the first version taking `Ts...` to be fully transparent. For the second, consider using `std::less` instead of `<` to get proper behavior for pointers. – Yakk - Adam Nevraumont Oct 06 '14 at 13:25
  • @Yakk I don't see much point in forwarding to `std::max` - it drops the value category on the ground anyway. `std::less` specialized for a pointer type provides a total order; `std::less` doesn't. – T.C. Oct 06 '14 at 13:35
  • @T.C. oh, `std::less` failing on pointers is sad. *sniff* – Yakk - Adam Nevraumont Oct 06 '14 at 13:36
  • @Yakk I suppose you could add an extra overload for pointers only...but it's a lot of complication for a scenario that `std::max` doesn't really support anyway... – T.C. Oct 06 '14 at 13:41
  • @T.C. could you tell why this is written as `t < u ? std::forward(u) : std::forward(t);` and not `std::forward(t) < std::forward(u) ? u : t` ? isn't the `?:` operator suppose to get `common_type` of operands, so that returning forward results in something *common* ? and shouldn't the forwarding forward the arguments to *some* `operator<` instead of calling it with plain `t` and `u` ? – Marc Andreson Oct 07 '14 at 15:00
  • @MarcAndreson Forwarding to `<` will break if `t` or `u` got moved from during the call to `operator<` - see Angew's comment above. You would be constructing the return value from a moved-from object. – T.C. Oct 07 '14 at 15:03
  • @T.C. ahh, and for the second issue, is there a point in forwarding operands, if the result is `common_type(t), type of std::forward(u)>` ? i can't think of what it actually does – Marc Andreson Oct 07 '14 at 15:11
  • @MarcAndreson It constructs the return value by moving when the selected argument is an rvalue. – T.C. Oct 07 '14 at 15:16
  • @MarcAndreson ?: and common_type give different types, the second one does a decay on the result of the first one. – Marc Glisse Oct 09 '14 at 07:58
  • @MarcGlisse ahh but I am not sure if `decltype(auto)` will work as expected due to this `?:` operator, as the declared return type must be something "common" for operands of `?:` which are references, maybe I am just wrong – Marc Andreson Oct 11 '14 at 17:00
  • @MarcAndreson Depending on the types and value categories of its second and third operands, the result of `?:` can be an lvalue, an xvalue or a prvalue. [The full set of rules](http://stackoverflow.com/a/24706610/2756719) is rather long. `decltype(auto)` will capture the value category appropriately. – T.C. Oct 11 '14 at 17:02
4

What is a workaround?

A lambda is probably the most readable and useful for predicates and comparators:

std::transform(v.begin(), v.end(), v2.begin(), v2.begin(),
               [] (int a, int b) {return std::max(a,b);} );

You might want to check out T.C.s functor if you need it more often. Or, with C++14:

auto max = [] (auto&& a, auto&& b) -> decltype(auto)
  {return a > b? std::forward<decltype(a)>(a) : std::forward<decltype(b)>(b);};

Why did the standard committee decide to do this knowing it would (probably) break existing code and force the user to create an ugly cast?

The only explanation is that they found the new overload to bring enough joy to compensate the breaking of existing code and the need for workarounds in future. You could just use std::max_element instead of this new overload, so you trade the syntax sugar for passing std::max-specializations as predicates for the syntax sugar of finding the maximum element within a couple of variables without explicitly creating an array for it.

Basically

std::transform( ..., std::max<int> );
// <=>
std::transform( ..., [] (int a, int b) {return std::max(a,b);} );

vs

int arr[] {a,b,c,d}; // You don't have an array with a,b,c,d included consecutively yet
int maximum = *std::max_element( std::begin(arr), std::end(arr) ); // ensure arr non-empty!
// <=>
auto maximum = std::max({a, b, c, d});

Maybe it does compensate? On the other hand, you barely ever need the latter.

Are future standards of C++ going to attempt to alleviate this problem?

I don't think so. Apparently, the standard committee really doesn't like to remove recently introduced features. I don't really see that much of a problem either; The lambda does the job.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
Columbo
  • 60,038
  • 8
  • 155
  • 203
  • There's not much point in forwarding if you are passing it over to `std::max`, since it throws the value category on the floor anyway. – T.C. Oct 06 '14 at 12:15
3

Although I've accepted T.C's answer which provides a comprehensive breakdown, as stated in a comment, I want to mimic the transparent comparator functor for class templates like std::less. This answer is provided for critique by others incase there's anything wrong with the syntax.

template <typename T = void>
struct my_max;

template <>
struct my_max<void> {
    template<class T, class U>
    constexpr decltype(auto) operator()( T&& t, U&& u ) const {
        return t < u ? std::forward<U>(u) : std::forward<T>(t);
    }
};
Community
  • 1
  • 1
user4112979
  • 312
  • 3
  • 11
0

If you are ok with using C++20 ranges additions you can just use std::ranges::max.

std::ranges::max is not a function, but a struct so it can be passed to the algorithms.

Example:

#include <iostream>
#include <algorithm>
#include <vector>
#include <fmt/ranges.h>

int main()
{
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);

    std::vector<int> v2;
    v2.push_back(2);
    v2.push_back(3);
    v2.push_back(4);

    std::transform(v.begin(), v.end(), v2.begin(), v2.begin(), std::ranges::max);
    std::cout << fmt::format("{}", v2);
}
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
-1

If you have recent boost you can use BOOST_HOF_LIFT macro.

fmt lib in example is just for printing vector, all you need is boost

#include <type_traits>
#include <cassert>
#include <iostream>
#include <algorithm>
#include <vector>
#include <boost/hof/lift.hpp>
#include <fmt/ranges.h>

int main()
{
    std::vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);

    std::vector<int> v2;
    v2.push_back(2);
    v2.push_back(3);
    v2.push_back(4);

    std::transform(v.begin(), v.end(), v2.begin(), v2.begin(), BOOST_HOF_LIFT(std::max<int>));
    std::cout << fmt::format("{}", v2);
}
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
  • using macro (especially from boost) is a pretty ugly solution. Also using BOOST in such a case implies introducing a huge and unnecessary dependency just to use `std::max` in `std::transform`, both of which are a part of std – Sergey Kolesnik Dec 07 '21 at 18:25
  • Boost is great, and widely avaliable so I disagree – NoSenseEtAl Dec 07 '21 at 19:42
  • Boost is not great, it is mostly outdated and is bloated with macros. Though nobody tells you that you shouldn't like it. That's an *opinion*. But introducing a huge library (BOOST is huge and it's a *fact*) as a dependency just for something a simple lambda can solve is such an overkill. It's not like using a canon where a hammer is enough. It's like using a Spanish Armada – Sergey Kolesnik Dec 07 '21 at 19:47
  • Nobody is forcing you to use entire boost. Many people are already using it so I dont consider this a huge problem. Obviously I doubt anybody would introduce boost just for HOF, but in my experience most C++ companies already use it... – NoSenseEtAl Dec 07 '21 at 19:58
  • in the context of this question *you* are the one who suggests introducing BOOST just for `HOF`. There was no mentioning of BOOST in the question – Sergey Kolesnik Dec 07 '21 at 20:01