29

While trying to play around with C++17 fold expressions, I tried to implement max sizeof where result is maximum of the sizeof of types. I have an ugly fold version that uses variable and a lambda, but I am unable to think of a way to use fold expressions and std::max() to get the same result.

This is my fold version:

template<typename... T>
constexpr size_t max_sizeof(){
    size_t max=0;
    auto update_max = [&max](const size_t& size) {if (max<size) max=size; };
    (update_max(sizeof (T)), ...);
    return max;
}


static_assert(max_sizeof<int, char, double, short>() == 8);
static_assert(max_sizeof<char, float>() == sizeof(float));
static_assert(max_sizeof<int, char>() == 4);

I would like to write equivalent function using fold expressions and std::max(). For example for 3 elements it should expand to

return std::max(sizeof (A), std::max(sizeof(B), sizeof (C)));

Is it possible to do that?

NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277

9 Answers9

28

Since nobody posted this one as an answer yet, the easiest way to do this with minimal effort is to just use the overload of std::max() that is ready-made for this problem: the one that takes an initializer_list:

template<typename... T>
constexpr size_t max_sizeof() {
    return std::max({sizeof(T)...});
}
Barry
  • 286,269
  • 29
  • 621
  • 977
26

Probably not what you wanted to hear, but no. It isn't possible to do that (purely1) with fold expressions. Their very grammar simply doesn't allow for it:

[expr.prim.fold]

A fold expression performs a fold of a template parameter pack over a binary operator.

fold-expression:
  ( cast-expression fold-operator ... )
  ( ... fold-operator cast-expression )
  ( cast-expression fold-operator ... fold-operator cast-expression )
fold-operator: one of
  +   -   *   /   %   ^   &   |   <<   >> 
  +=  -=  *=  /=  %=  ^=  &=  |=  <<=  >>=  =
  ==  !=  <   >   <=  >=  &&  ||  ,    .*   ->*

Simply because a function call expression is not a binary operator in the pure grammar sense.


1 Refer to the other superb answers.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
19

If you want to use fold expressions here then you need to somehow use an operator to invoke std::max rather than a function call. Here's an example abusing operator^ to that end:

namespace detail {
    template<typename T, std::size_t N = sizeof(T)>
    struct type_size : std::integral_constant<std::size_t, N> { };

    template<typename T, auto M, typename U, auto N>
    constexpr auto operator ^(type_size<T, M>, type_size<U, N>) noexcept {
        return type_size<void, std::max(M, N)>{};
    }
}

template<typename... T>
constexpr std::size_t max_sizeof() noexcept {
    using detail::type_size;
    return (type_size<T>{} ^ ... ^ type_size<void, 0>{});
    // or, if you don't care to support empty packs
    // return (type_size<T>{} ^ ...);
}

Online Demo


EDIT: @Barry's suggestion of removing T from type_size (renamed max_val here):

namespace detail {
    template<auto N>
    struct max_val : std::integral_constant<decltype(N), N> { };

    template<auto M, auto N, auto R = std::max(M, N)>
    constexpr max_val<R> operator ^(max_val<M>, max_val<N>) noexcept {
        return {};
    }
}

template<typename... T>
constexpr std::size_t max_sizeof() noexcept {
    using detail::max_val;
    return (max_val<sizeof(T)>{} ^ ... ^ max_val<std::size_t{}>{});
    // or, if you don't care to support empty packs
    // return (max_val<sizeof(T)>{} ^ ...);
}

Online Demo

Externally, both implementations are equivalent; in terms of implementation, I personally prefer the former, but YMMV. :-]

ildjarn
  • 62,044
  • 9
  • 127
  • 211
  • 3
    @NoSenseEtAl : C++/CLI has `^` to designate handles, which are the managed equivalent to pointers, but it is not an operator (C++/CLI does add an operator though, `operator%`, equivalent to native unary `operator*`). `^` is a binary operator which does bitwise XOR by default. – ildjarn Sep 25 '17 at 12:14
  • 2
    Could just use `template struct type_size : integral_constant { }` and avoid having to use `void`. – Barry Sep 25 '17 at 15:33
  • @Barry : Sure, I was just trying to avoid having to use `sizeof` inside of `max_sizeof` itself. Your approach would be more succinct overall, but I was trying for some small attempt at (subjective) readability. :-P – ildjarn Sep 25 '17 at 15:35
  • 1
    Anyway feel free to absorb elements from my answer if you like it; en passant: are you sure it's necessary the `^ type_size{}` part? – max66 Sep 25 '17 at 17:46
10

Just to play with c++17 fold expressions

template <typename ... Ts>
constexpr std::size_t max_sizeof ()
 {
   std::size_t  ret { 0 };

   return ( (ret = (sizeof(Ts) > ret ? sizeof(Ts) : ret)), ... ); 
 }

or, using the fact that std::max() is constexpr starting from C++14 (so it is in C++17)

template <typename ... Ts>
constexpr std::size_t max_sizeof ()
 {
   std::size_t  ret { 0 };

   return ( (ret = std::max(sizeof(Ts), ret)), ... ); 
 }

Not really different from your original version.

max66
  • 65,235
  • 10
  • 71
  • 111
  • 1
    This is not easy to read... Is there no way to rewrite this as a fold-recursive form? – YSC Sep 25 '17 at 11:59
  • I gave you upvote for being technically correct, but I am hoping somebody can think of nicer solution since like YSC Ihave trouble parsing this. I mean I can guess what it does now since I asked this question, but if this code came to me for review I would be confused. :) – NoSenseEtAl Sep 25 '17 at 12:02
  • @YSC - well... not really brilliant but (IMHO) for a skilled C++ programmer should be easy to read; anyway, I've added a version that use `std::max()` that should be easier; but... what do you mean with "fold-recursive form" – max66 Sep 25 '17 at 12:07
  • 3
    @NoSenseEtAl - unfortunately the new c++17 fold expression works with operators; the best I can imagine is the use of comma, as operator, to iterate something over every `sizeof()`; the "something" can be an assignment based over a ternary operator or `std::max()` or other but I don't think is possible to do a lot better. – max66 Sep 25 '17 at 12:10
  • 3
    @NoSenseEtAl - anyway, look at the brilliant answer from ildjarn: he redefine an operator to obtain what do you want. – max66 Sep 25 '17 at 12:15
3

Sure, no problem.

template<class Lhs, class F>
struct foldable_binop_t {
  Lhs lhs;
  F f;
  template<class Rhs>
  auto operator*(Rhs&& rhs) &&
  -> foldable_binop_t< std::result_of_t<F&(Lhs&&, Rhs&&)>, F >
  {
    return { f(std::forward<Lhs>(lhs), std::forward<Rhs>(rhs)), std::forward<F>(f) };
  }
  Lhs operator()() && { return std::forward<Lhs>(lhs); }
  operator Lhs() && { return std::move(*this)(); }
  Lhs get() && { return std::move(*this); }
};
template<class F>
struct foldable_t {
  F f;
  template<class Lhs>
  friend foldable_binop_t<Lhs, F> operator*( Lhs&& lhs, foldable_t&& self ) {
    return {std::forward<Lhs>(lhs), std::forward<F>(self.f)};
  }
  template<class Rhs>
  foldable_binop_t<Rhs, F> operator*( Rhs&& rhs ) && {
    return {std::forward<Rhs>(rhs), std::forward<F>(f)};
  }
};
template<class F>
foldable_t<F> foldable(F f) { return {std::move(f)}; }

test code:

template<class...Xs>
auto result( Xs... xs ) {
  auto maxer = [](auto&&...args){return (std::max)(decltype(args)(args)...);};
  return ((0 * foldable(maxer)) * ... * xs).get();
}
template<class...Xs>
auto result2( Xs... xs ) {
  auto maxer = [](auto&&...args){return (std::max)(decltype(args)(args)...);};
  return (foldable(maxer) * ... * xs).get();
}

int main() {
  int x = result2( 0, 7, 10, 11, -3 ); // or result
  std::cout << x << "\n";
}

Live example.

Personally I find

  auto maxer = [](auto&&...args){return (std::max)(decltype(args)(args)...);};

annoying to write all the time, so

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__( decltype(args)(args)... ) )

makes it

template<class...Xs>
auto result3( Xs... xs ) {
  return (foldable(OVERLOADS_OF((std::max))) * ... * xs).get();
}

or even

template<class...Xs>
constexpr auto result4( Xs... xs )
  RETURNS( (foldable(OVERLOADS_OF((std::max))) * ... * xs).get() )

which is seems more expressive, and gets noexcept / constexpr right, etc.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

I would like to write equivalent function using fold expressions and std::max. For example for 3 elements it should expand to

return std::max(sizeof (A), std::max(sizeof(B), sizeof (C)));

Another possible solution (based on recursion, not of fold expression) is the following

template <typename T0>
constexpr std::size_t max_sizeof ()
 { return sizeof(T0); }
    
template <typename T0, typename T1, typename ... Ts>
constexpr std::size_t max_sizeof ()
 { return std::max(sizeof(T0), max_sizeof<T1, Ts...>()); }
Community
  • 1
  • 1
max66
  • 65,235
  • 10
  • 71
  • 111
  • I considered that but now I prefer to write my functions without special case when I can, IIRC that was one of the selling points of fold expressions. – NoSenseEtAl Sep 25 '17 at 16:11
  • 1
    @NoSenseEtAl - I see... but the absence of the special case with fold expression (in your particular problem) is only apparent: correspond to the inizialization to zero for `ret`, in my other answer, or the object of type `type_size{}` in ildjarn solutions. Anyway, I think that a solution based on fold expression is preferable because there are limits in template recursion. – max66 Sep 25 '17 at 16:23
1

Not a fold expression, but another way that c++17 offers - if constexpr:

template<class X, class Y, class...Ts>
constexpr std::size_t max_sizeof()
{
    auto base = std::max(sizeof(X), sizeof(Y));

    if constexpr (sizeof...(Ts) == 0)
    {
        // nothing
    }
    else if constexpr (sizeof...(Ts) == 1)
    {
        base = std::max(base, sizeof(Ts)...);
    }
    else
    {
        base = std::max(base, max_sizeof<Ts...>());
    }
    return base;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
1

Just for fun, a variation on the theme on the brilliant solution from ildjarn

namespace detail
 {
   template <std::size_t N>
   struct tSizeH : std::integral_constant<std::size_t, N> { };

   template <std::size_t M, std::size_t N>
   constexpr tSizeH<std::max(M, N)> operator^ (tSizeH<M>, tSizeH<N>);
 }

template <typename ... T>
constexpr std::size_t max_sizeof() noexcept
 { return decltype((detail::tSizeH<sizeof(T)>{} ^ ...))::value; }

A little simplified because (a) the helper class use only the sizeof() of the type (resolved directly in max_sizeof(), (b) no use of the terminal value based on void and zero, (c) the operator^() is declared but unimplemented (there is no need of implement it: interest only for the return type) and (d) max_sizeof() use decltype() instead of calling operator^() (so there is no need of implement it).

max66
  • 65,235
  • 10
  • 71
  • 111
  • If you implemented it (which is just `return {}`) then your body could just be `return (detail::tSizeH{} ^ ...);` – Barry Sep 25 '17 at 17:09
  • 1
    @Barry - yes; but my idea is use `decltype()` in the body (I repeat: just for fun). – max66 Sep 25 '17 at 17:12
0

How about this (courtesy of https://articles.emptycrate.com/2016/05/14/folds_in_cpp11_ish.html):

template<typename U, typename ... V>
constexpr auto max(const U &u, const V &... v) -> typename std::common_type<U, V...>::type {
  using rettype = typename std::common_type<U, V...>::type;
  rettype result = static_cast<rettype>(u);
  (void)std::initializer_list<int>{ ( (v > result)?(result = static_cast<rettype>(v), 0):0 )... };
  return result;
}

works in c++14 so it doesn't use c++17 fold expressions, but it works as shown here: https://godbolt.org/z/6oWvK9

schrödinbug
  • 692
  • 9
  • 19