Can I implement max(A, max(B, max(C, D))) using fold expressions?

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.


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)...});
}

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