13

In this interview Stepanov shows how to implement generic max function in C++.

Try to implement a simple thing in the object oriented way, say, max. I do not know how it can be done. Using generic programming I can write:

template <class StrictWeakOrdered>
inline StrictWeakOrdered& max(StrictWeakOrdered& x,
StrictWeakOrdered& y) {
return x < y ? y : x;
}

and
template <class StrictWeakOrdered>
inline const StrictWeakOrdered& max(const StrictWeakOrdered& x,
const StrictWeakOrdered& y) {
return x < y ? y : x;
}

(you do need both & and const &).

Why is there need to write the code twice? Is this needed to aid compiler for optimization or a convention to reduce bugs? Is max a special case where body of a const version is identical?

How many valid const and non-const permutations a function of N arguments should have to define a complete API?

sevo
  • 4,559
  • 1
  • 15
  • 31
  • 2
    If you didn't have the const version, you couldn't call the function with const objects. Likewise, if you had non-const objects, you couldn't continue to treat them as non-const after the call if you only had the const version of the function. You need both. I wouldn't consider it a duplicate, but http://stackoverflow.com/questions/3582001/advantages-of-using-forward has not only more explanation, but the answer to your "how do I do this in my API" question. In C++11, you only need one function. – GManNickG Jan 31 '16 at 21:46
  • The reason is that we want the return value's const-ness to match the arguments' const-ness and there's currently no easy way to do that in C++ – M.M Jan 31 '16 at 23:23
  • @GManNickG You could call the non-const version with a const object (in that case `StrictWeakOrdered` is deduced to be a const-qualified type). The problem is that you can't call it with non-const rvalues (i.e., `max(1, 2)` won't work). – T.C. Feb 02 '16 at 02:30

3 Answers3

7

First of all, you need the non-const version to allow stuff like

max(a, b) = something;

If you don't want to do such things, you can just provide the const version only to cover all cases. That is basically what the standard std::max does.

You also do not need to provide any more permutations of const and non-const, returning non-const& only makes sense if all inputs are non-const, all other cases are properly handled by the const version.

If you want to avoid code duplication, you can do something like this:

template <class StrictWeakOrdered>
inline StrictWeakOrdered& max(StrictWeakOrdered& x, StrictWeakOrdered& y) {
    const auto &xr = x;
    const auto &yr = y;
    return const_cast<StrictWeakOrdered&>(max(xr, yr));
}

In this special case, the const_cast is safe because you already know that the input is really non-const. Now you only have to provide the implementation for the const case.

So providing the implementation twice is not required and should not help the compiler, but whether or not the above is more readable than what Stepanov did is debatable.

Baum mit Augen
  • 49,044
  • 25
  • 144
  • 182
  • I would restrict `const_cast`to be used to solve compatibility issues with legacy code which is not const-correct. Otherwise, I would flag it as bad style, especially as there are better alternatives. – Jens Jan 31 '16 at 22:00
  • @Jens Except that the better alternative [does not work](http://coliru.stacked-crooked.com/a/a187022c81668acf). ;) – Baum mit Augen Jan 31 '16 at 22:03
  • Perhaps we need to agree on what it must support. I didn't consider that breakage. It's proper diagnostics on unintended use :) – sehe Jan 31 '16 at 23:19
  • @sehe Being able to print the maximum of a variable and a value if kind of natural IMO. – Baum mit Augen Jan 31 '16 at 23:31
  • I feel it should be possible to provide a simple overload for different value cats. Of course that has little to do with the original question. You're right, it's not 1 overload to do it all. It bores me to think about how to improve it :) – sehe Jan 31 '16 at 23:50
7

You actually don't need both versions. You can write it this way.

template <class S, class T>
decltype(auto) max(S&& a, T&& b) {
    using Ret = std::conditional_t<
          std::is_const<std::remove_reference_t<S>>::value, S, T>;

    if (b < a)
        return std::forward<Ret>(a);
    return std::forward<Ret>(b);
}

Falling back to const if either of the arguments was const.

Yam Marcovic
  • 7,953
  • 1
  • 28
  • 38
  • You guys are making this simple problem way too hard. :) http://coliru.stacked-crooked.com/a/8a6bf54ebd176c05 – Baum mit Augen Jan 31 '16 at 22:53
  • 1
    @BaummitAugen Well, reducing duplication is a noble cause. :) – Yam Marcovic Jan 31 '16 at 23:15
  • 1
    Took a while, but got a dangling reference and a non-sense assignment out of it. Now whether this is really a bug is debatable, your code looks good enough. http://coliru.stacked-crooked.com/a/52425289d91380e6 http://coliru.stacked-crooked.com/a/db4999b49dd70051 – Baum mit Augen Jan 31 '16 at 23:37
1

If you do not intend to modify the argument, you can just go with the const& version. Everything should bind to a const reference.

C++11 also introduced reference collapsing, and a template parameter T&& is sometimes called a universal reference. In this case, when instantiating the parameter type for e.g. a int&, we would have int& && which collapses to int&. Now, you can write the function as

template <class T1, class T2>
inline T1 const& max(T1&& x, T2&& y)  {
     T1 const& x_=x;
     T2 const& y_=y;
     return (x_ < y_) ? (y_) : (x_);
}

This can be called with const values, temporaries (r-values) and mutable variables:

int const a=1;
int b=2;
max(b,b) = 23;
std::cout << max(a,a) << max( int{4}, int{5} ) << b << max(int{4}, a);
Jens
  • 9,058
  • 2
  • 26
  • 43
  • Sadly, this does not quite work. http://coliru.stacked-crooked.com/a/a187022c81668acf – Baum mit Augen Jan 31 '16 at 22:10
  • @BaummitAugen What would be the return type of `max(non_const_int, const_int)? A const reference or a non-const reference? I think it depends on the values of the arguments, so you cannot write it down statically in either way. – Jens Jan 31 '16 at 22:35
  • The return type for that should definitely be `const&`. Allowing `max(a,b) = 4` only if `b > a` does not really make sense, does it? (Even if it was possible.) – Baum mit Augen Jan 31 '16 at 22:37
  • Then I can use universal references to accept everything, and use a temporary const ref in the function to fix the return type. We can make the return type depend on the argument types and return a non-const reference if neither T1 or T2 are const, but I skipped that. – Jens Jan 31 '16 at 22:40
  • I know universal references (or forwarding references) are new and shiny, but they do not really seem to make things easier in this case. :) (Oh, and not my DV btw) – Baum mit Augen Jan 31 '16 at 22:49
  • The problem in this case is not the universal references but the return type. If you change it to auto, your example will compie. – Jens Jan 31 '16 at 22:51