Enhanced fold expressions

135 views
Skip to first unread message

Roman Orlov

unread,
Mar 8, 2017, 2:11:27 PM3/8/17
to ISO C++ Standard - Future Proposals
At this moment fold expressions have 4 forms
  1. ( pack op ... )
  2. ( ... op pack )
  3. ( pack op ... op init )
  4. ( init op ... op pack )
where 'op' is one of the following 32 binary operators:

+ - * / % ˆ & | << >> += -= *= /= %= ˆ= &= |= <<= >>= = == != < > <= >= && || , .* ->*

It is insufficient for writing powerful fold expressions. So it is impossible to find min/max element in compile-time sequence of integers.

template<typename T, T... Args>
constexpr T min(std::integer_sequence<T, Args...>)
{
   
return (... ??? Args); // what operator should be here?
}

template<typename T, T... Args>
constexpr T max(std::integer_sequence<T, Args...>)
{
   
return (... ??? Args); // what operator should be here?
}

To solve that we need binary function f(x,y) returning either x or y. And there is no standard binary operator for doing this.
But we already have these functions in algorithm library 

template< class T >
constexpr const T& min( const T& a, const T& b );
template< class T >
constexpr const T& max( const T& a, const T& b );

I suggest to extend the meaning of binary operator in fold expression. Operator is a function, so any constexpr binary function could be used in fold expression.
I've invented a little hack to proof the idea

#include <iostream>
#include <algorithm>

// it's a user defined type convertible to int
enum dummy_int: int {};
// now we can define operators for it

// it's a constexpr wrapper around min function
constexpr dummy_int const& operator << (dummy_int const& lhs, dummy_int const& rhs)
{
   
return std::min(lhs, rhs);
}

// it's a constexpr wrapper around max function
constexpr dummy_int const& operator >> (dummy_int const& lhs, dummy_int const& rhs)
{
   
return std::max(lhs, rhs);
}

template<typename T, T... Args>
constexpr T min(std::integer_sequence<T, Args...>)
{
   
return (... << dummy_int(Args)); // explicitly convert to dummy_int
}

template<typename T, T... Args>
constexpr T max(std::integer_sequence<T, Args...>)
{
   
return (... >> dummy_int(Args)); // explicitly convert to dummy_int
}

int main()
{
   
typedef std::integer_sequence<int, 3, 4, 1, 2> ints;
   
// to be sure in compile-time evaluation use the result as template parameter
    std
::cout << std::integral_constant<int, min(ints())>::value << std::endl;
    std
::cout << std::integral_constant<int, max(ints())>::value << std::endl;
   
return 0;
}

The code would be simpler if I can write it like this

template<typename T, T... Args>
constexpr T min(std::integer_sequence<T, Args...>)
{
   
return (... std::min Args);
}

template<typename T, T... Args>
constexpr T max(std::integer_sequence<T, Args...>)
{
   
return (... std::max Args);
}

Arthur O'Dwyer

unread,
Mar 8, 2017, 5:58:18 PM3/8/17
to ISO C++ Standard - Future Proposals
On Wednesday, March 8, 2017 at 11:11:27 AM UTC-8, Roman Orlov wrote:
[TLDR: folding over arbitrary binary functions] 
I suggest to extend the meaning of binary operator in fold expression. Operator is a function, so any constexpr binary function could be used in fold expression.
I've invented a little hack to proof the idea

(Minor point: constexpr is completely irrelevant to your use-case. You should drop the mention of a constexpr requirement.)

This hack is not new. See e.g.
 
template<typename T, T... Args>
constexpr T min(std::integer_sequence<T, Args...>)
{
   
return (... << dummy_int(Args)); // explicitly convert to dummy_int
}

Using my version of the hack, you would write

template<typename T, T... Args>
constexpr T max(std::integer_sequence<T, Args...>)
{

   
auto mmax = make_named_operator([](auto&& a, auto&& b){ return std::max(a,b); });
   
return (... + mmax(Args));
}

Nick Athanasiou's original blog post takes the more functional-programming approach: that what you should really write is

    return foldl([](auto&& a, auto&& b){ return std::max(a,b); }, Args...);


The code would be simpler if I can write it like this

template<typename T, T... Args>
constexpr T max(std::integer_sequence<T, Args...>)
{
   
return (... std::max Args);
}

Agreed that that might look simpler (and you could add a code comment with that syntax if you think it'd help the reader); but I don't see how you expect the compiler to figure out what you mean by that syntax, unambiguously. For example, what is

    return (... std::max (Args));
    return (... std::max<int> Args);
    return (... func_ptr Args);
    return (... (*func_ptr) Args);
    return (... func_ptr*Args);
    return (... (*Args) Args);

Any time you take a piece of the grammar that currently accepts a closed set of operators and propose changing it to accept an open set of expressions, that's a red flag; or at least an indication that you need to think really hard about whether there might be any corner cases.

FYI, Barry Revzin's P0573R0 (which I am generally against) mentions (but does not propose) the syntax []std::max as a shortcut way of writing [](auto&&... args) -> decltype(auto) { return std::max(args...); }. If we had that, not only would the two alternatives presented above get a lot shorter, but you wouldn't have to worry quite so much about the fact that the identifier std::max by itself doesn't denote any particular entity (but rather just an overload set of a bunch of otherwise unrelated entities). You could design your syntax to work cleanly with []foo by design.

my $.02,
Arthur
Reply all
Reply to author
Forward
0 new messages