Skip to main content
Implemented to use standard-library ranges as you did.
Source Link
Davislor
  • 9.2k
  • 19
  • 39

This is a bug with (I believe) the libstdc++ implementation of std::ranges::subrange. If you upgrade from Clang 15.0.0 to the latest trunk branch, it will fix this and give you another bug with your partial template specializations.

Based on your idea to restrict the containers with the std::range::input_range concept, I realized that constructing a subrange object is not needed.

#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;std::ranges::input_range<T>;
    std::constructible_from< decltype(std::ranges::begin(x)),
                             decltype(std::ranges::end(x)) >;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<Tstd::ranges::input_range<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } ); 

    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                       "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputTranges::iteratoroutput_range<OutputT, U>,
                       "The return object must be writable." );

        OutputT output;
        // Sanity check that the container is enough like a std::array:
        assert(std::ranges::size(output) == std::ranges::size(resultsinput));

/* The results of the transform_view should be movable, since passing an
 * identity funtion that returns move references to this algorithm would be
 * absurd.
 */
        std::move( results.begin(), results.end(), output.std::ranges::begin(output) );
        return output;
    }
}

compiles on Godbolton Godbolt (also showing which version of the compiler you need to use) to the constants

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};


/* An overload for functionscontainers similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<Tstd::ranges::input_range<Container< T, A<T>>>A<T> >> &&
             constructible_from_iterators<Container< T, A<T> >> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             std::ranges::input_range<Container< K, C<K>, A<K> >> &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

template<class T>
concept sized = requires(T x) {
    {std::size(x)} -> std::unsigned_integral;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;std::ranges::input_range<T>;
    std::constructible_from< decltype(std::ranges::begin(x)),
                             decltype(std::ranges::end(x)) >;
};

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};

/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* Forward declarations for nesting.of */
template<the typenamerecursive Toverloads,
        to allow typenamenesting Fin >any
    requires std::regular_invocable<F,* T>order.
constexpr auto traverse( const T& input, const F& f );
*/
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<Tstd::ranges::input_range<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f );

template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<Tstd::ranges::input_range<Container< T, A<T>>>A<T> >> &&
             constructible_from_iterators<Container< T, A<T> >> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f );

template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             std::ranges::input_range<Container< K, C<K>, A<K> >> &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f );

 
/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<Tstd::ranges::input_range<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
 
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                       "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputTranges::iteratoroutput_range<OutputT, U>,
                       "The return object must be writable." );

        OutputT output;
        // Sanity check that the container is enough like a std::array:
        assert(std::ranges::size(output) == std::ranges::size(resultsinput));

/* The results of the transform_view should be movable, since passing an
 * identity funtion that returns move references to this algorithm would be
 * absurd.
 */
        std::move( results.begin(), results.end(), output.std::ranges::begin(output) );
        return output;
    }
}


/* An overload for functionscontainers similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<Tstd::ranges::input_range<Container< T, A<T>>>A<T> >> &&
             constructible_from_iterators<Container< T, A<T> >> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             std::ranges::input_range<Container< K, C<K>, A<K> >> &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}

And here again is the link to the code on Godbolt.the code on Godbolt.

This is a bug with (I believe) the libstdc++ implementation of std::ranges::subrange. If you upgrade from Clang 15.0.0 to the latest trunk branch, it will fix this and give you another bug with your partial template specializations.

#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}

compiles on Godbolt (also showing which version of the compiler you need to use) to the constants

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

template<class T>
concept sized = requires(T x) {
    {std::size(x)} -> std::unsigned_integral;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};

/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* Forward declarations for nesting. */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f );

template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f );

template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f );

template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f );

 
/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}

And here again is the link to the code on Godbolt.

This is a bug with (I believe) the libstdc++ implementation of std::ranges. If you upgrade from Clang 15.0.0 to the latest trunk branch, it will fix this and give you another bug with your partial template specializations.

Based on your idea to restrict the containers with the std::range::input_range concept, I realized that constructing a subrange object is not needed.

#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <ranges>     // transform_view
#include <utility>    // declval

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    std::ranges::input_range<T>;
    std::constructible_from< decltype(std::ranges::begin(x)),
                             decltype(std::ranges::end(x)) >;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             std::ranges::input_range<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );

    const auto results = std::ranges::transform_view(
        input,
        [&f](const T& x)constexpr{ return traverse(x, f); } ); 

    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        static_assert( std::default_initializable<OutputT>,
                       "Must be able to create a return object." );
        static_assert( std::ranges::output_range<OutputT, U>,
                       "The return object must be writable." );

        OutputT output;
        // Sanity check that the container is enough like a std::array:
        assert(std::ranges::size(output) == std::ranges::size(input));

/* The results of the transform_view should be movable, since passing an
 * identity funtion that returns move references to this algorithm would be
 * absurd.
 */
        std::move( results.begin(), results.end(), std::ranges::begin(output) );
        return output;
    }
}

compiles on Godbolt (also showing which version of the compiler you need to use) to the constants

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};


/* An overload for containers similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             std::ranges::input_range<Container< T, A<T> >> &&
             constructible_from_iterators<Container< T, A<T> >> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        input,
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             std::ranges::input_range<Container< K, C<K>, A<K> >> &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        input,
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <ranges>     // transform_view
#include <utility>    // declval

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    std::ranges::input_range<T>;
    std::constructible_from< decltype(std::ranges::begin(x)),
                             decltype(std::ranges::end(x)) >;
};

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};

/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* Forward declarations of the recursive overloads, to allow nesting in any
 * order.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             std::ranges::input_range<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f );

template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             std::ranges::input_range<Container< T, A<T> >> &&
             constructible_from_iterators<Container< T, A<T> >> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f );

template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             std::ranges::input_range<Container< K, C<K>, A<K> >> &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f );

/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             std::ranges::input_range<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );

    const auto results = std::ranges::transform_view(
        input,
        [&f](const T& x)constexpr{ return traverse(x, f); } );
 
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        static_assert( std::default_initializable<OutputT>,
                       "Must be able to create a return object." );
        static_assert( std::ranges::output_range<OutputT, U>,
                       "The return object must be writable." );

        OutputT output;
        // Sanity check that the container is enough like a std::array:
        assert(std::ranges::size(output) == std::ranges::size(input));

/* The results of the transform_view should be movable, since passing an
 * identity funtion that returns move references to this algorithm would be
 * absurd.
 */
        std::move( results.begin(), results.end(), std::ranges::begin(output) );
        return output;
    }
}


/* An overload for containers similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             std::ranges::input_range<Container< T, A<T> >> &&
             constructible_from_iterators<Container< T, A<T> >> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        input,
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             std::ranges::input_range<Container< K, C<K>, A<K> >> &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert( std::ranges::input_range<OutputT>,
                   "Could not deduce the return type." );
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        input,
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}

And here again is the link to the code on Godbolt.

added 901 characters in body
Source Link
Davislor
  • 9.2k
  • 19
  • 39

Update: the Helper Template

You currently are using the type recursive_invoke_result_t with the type constraint std::ranges::input_range to deduce the return type of the invocations. That’s better than what I did in several ways (using the concept from the standard library is better than reinventing the wheel like me), but it does have one disadvantage.

If you ever need to support a container with an irregular return type under the transformation—such as transforming a C-style array into a std::array because a function cannot return the former, or transforming a std::basic_string<T> into a std::vector<U> when U is not a character type—you would also need to overload your helper template, and always keep it consistent with the function templates elsewhere. If you do the type deduction within the functions themselves, the necessary code is all in one place.

Update: the Helper Template

You currently are using the type recursive_invoke_result_t with the type constraint std::ranges::input_range to deduce the return type of the invocations. That’s better than what I did in several ways (using the concept from the standard library is better than reinventing the wheel like me), but it does have one disadvantage.

If you ever need to support a container with an irregular return type under the transformation—such as transforming a C-style array into a std::array because a function cannot return the former, or transforming a std::basic_string<T> into a std::vector<U> when U is not a character type—you would also need to overload your helper template, and always keep it consistent with the function templates elsewhere. If you do the type deduction within the functions themselves, the necessary code is all in one place.

Fixed bug with F whose domain is a container.
Source Link
Davislor
  • 9.2k
  • 19
  • 39

Update: What if Your Funtion Takes a Container?

This was a bug you copied from me: previously, if you passed in a transformation function that takes a container, such as, it would fail:

static const std::vector<std::array<int, 2>> test_data4 =
    { {1, 0}, {0, 1} };
const auto test_result4 = traverse( test_data4,
                                    [](const std::array<int, 2>& v)constexpr{ return v[0]+v[1]; } 
                                  );

Since this would incorrectly match the overload for traverse( const Container<T, N>&, const F& ). The fix here is to add a new requirement to all the recursive overloads, disabling them if they also match the base case (that is, if the container they match is invokable by F). I’ve edited this in below.

#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}

compiles on Godbolton Godbolt (also showing which version of the compiler you need to use) to the constants

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

template<class T>
concept sized = requires(T x) {
    {std::size(x)} -> std::unsigned_integral;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};

 
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}


/* Forward declarations for nesting. */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f );

template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f );

template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f );

template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f );
 

/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <array>
#include <iostream>
#include <set>
#include <stdlib.h>
#include <string.h>
#include <vector>

using std::cerr, std::cout, std::endl, std::size_t;

/* A classic ASCII-only lowercase function, */
constexpr char8_t lowercase(const char c) noexcept
{
    return static_cast<char8_t>(( c >= 'A' && c <= 'Z' ) ?
                                (c + ('a' - 'A')) :
                                c);
}


int main()
{
    { // Test 1:
        static constexpr std::array<unsigned, 5> test_data1 = { 1, 2, 3, 4, 5 };
        static constexpr auto test_result1 = traverse( test_data1,
                                                       [](const unsigned& x)constexpr{ return static_cast<double>(x); }
                                                     );

        for ( size_t i = 0; i < test_result1.size(); ++i ) {
          expect_test(test_data1[i], test_result1[i]);
        }
    }

    { // Test 2:
        static const std::vector<char> test_data2 = {'H','e','l','l','o',',',' ','W','O','R','L','D','!','\0'};
        static const auto test_result2 = traverse( test_data2,
                                                   &lowercase );
        static constexpr char8_t expected2[] = u8"hello, world!";
        
        if ( test_result2.size() == sizeof(expected2) &&
             memcmp( test_result2.data(), expected2, sizeof(expected2) ) == 0 ) {
           cout << "Test 2 passed." << endl;
        } else {
           cout.flush();
           cerr << "Test 2 FAILED with: "
                << reinterpret_cast<const char*>(test_result2.data())
                << endl;
        }
    }

    { // Test 3:
        static const std::set<int> test_data3 = {2,3,4,5,6};
        static const auto test_result3 = traverse( test_data3, std::negate<int>{} );
        static const std::set<int> expected3 = {-2,-3,-4,-5,-6};
 
        if (test_result3 == expected3) {
            cout << "Test 3 passed." << endl;
        } else {
            cerr << "Test 3 FAILED." << endl;
        }
    }

    { // Test 4:
        static const std::vector<std::array<int, 2>> test_data4 =
          { {1, 0}, {0, 1} };
        const auto test_result4 = traverse( test_data4,
                                            [](const std::array<int, 2>& v)constexpr{ return v[0]+v[1]; } 
                                          );
        static const std::vector<int> expected4 = {1, 1};

        if (test_result4 == expected4) {
            cout << "Test 4 passed." << endl;
        } else {
            cerr << "Test 4 FAILED." << endl;
        }
    }

    return EXIT_SUCCESS;
}

And here again is the link to the code on Godbolt.the code on Godbolt.

#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}

compiles on Godbolt (also showing which version of the compiler you need to use) to the constants

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

template<class T>
concept sized = requires(T x) {
    {std::size(x)} -> std::unsigned_integral;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};

 
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}


/* Forward declarations for nesting. */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f );

template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f );

template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f );

template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f );

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <array>
#include <iostream>
#include <set>
#include <stdlib.h>
#include <string.h>
#include <vector>

using std::cerr, std::cout, std::endl, std::size_t;

/* A classic ASCII-only lowercase function, */
constexpr char8_t lowercase(const char c) noexcept
{
    return static_cast<char8_t>(( c >= 'A' && c <= 'Z' ) ?
                                (c + ('a' - 'A')) :
                                c);
}


int main()
{
    {
        static constexpr std::array<unsigned, 5> test_data1 = { 1, 2, 3, 4, 5 };
        static constexpr auto test_result1 = traverse( test_data1,
                                                       [](const unsigned& x)constexpr{ return static_cast<double>(x); }
                                                     );

        for ( size_t i = 0; i < test_result1.size(); ++i ) {
          expect_test(test_data1[i], test_result1[i]);
        }
    }

    {
        static const std::vector<char> test_data2 = {'H','e','l','l','o',',',' ','W','O','R','L','D','!','\0'};
        static const auto test_result2 = traverse( test_data2,
                                                   &lowercase );
        static constexpr char8_t expected2[] = u8"hello, world!";
        
        if ( test_result2.size() == sizeof(expected2) &&
             memcmp( test_result2.data(), expected2, sizeof(expected2) ) == 0 ) {
           cout << "Test 2 passed." << endl;
        } else {
           cout.flush();
           cerr << "Test 2 FAILED with: "
                << reinterpret_cast<const char*>(test_result2.data())
                << endl;
        }
    }

    {
        static const std::set<int> test_data3 = {2,3,4,5,6};
        static const auto test_result3 = traverse( test_data3, std::negate<int>{} );
        static const std::set<int> expected3 = {-2,-3,-4,-5,-6};
 
        if (test_result3 == expected3) {
            cout << "Test 3 passed." << endl;
        } else {
            cerr << "Test 3 FAILED." << endl;
        }
    }

    return EXIT_SUCCESS;
}

And here again is the link to the code on Godbolt.

Update: What if Your Funtion Takes a Container?

This was a bug you copied from me: previously, if you passed in a transformation function that takes a container, such as, it would fail:

static const std::vector<std::array<int, 2>> test_data4 =
    { {1, 0}, {0, 1} };
const auto test_result4 = traverse( test_data4,
                                    [](const std::array<int, 2>& v)constexpr{ return v[0]+v[1]; } 
                                  );

Since this would incorrectly match the overload for traverse( const Container<T, N>&, const F& ). The fix here is to add a new requirement to all the recursive overloads, disabling them if they also match the base case (that is, if the container they match is invokable by F). I’ve edited this in below.

#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};


/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}

compiles on Godbolt (also showing which version of the compiler you need to use) to the constants

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <algorithm>  // move
#include <cassert>
#include <concepts>
#include <cstddef>    // size_t
#include <functional> // invoke
#include <iterator>   // std::begin, std::end
#include <ranges>     // transform_view
#include <utility>    // declval

template<class T>
concept iterable = requires(T x) {
    {std::begin(x)} -> std::input_iterator;
    {std::end(x)} -> std::sentinel_for<decltype(std::begin(x))>;
};

template<class T>
concept sized = requires(T x) {
    {std::size(x)} -> std::unsigned_integral;
};

/* A container or range constructible from its own iterator and sentinel
 * types.
 */
template<class T>
concept constructible_from_iterators = requires(T x) {
    iterable<T>;
    std::constructible_from< decltype(std::begin(x)),
                             decltype(std::end(x)) >;
};

/* Matches the interface of an allocator, such as std::allocate<T>.
 */
template<class A, typename T>
concept allocator = requires(A a, std::size_t n) {
    {a.allocate(n)} -> std::same_as<T*>;
    {a.deallocate(a.allocate(n), n)} -> std::same_as<void>;
};

/* Matches the interface of a comparator for T, such as std::less<T>
 */
template<class C, typename T>
concept comparator = requires(T x, C c) {
    {c(x,x)} -> std::convertible_to<bool>;
};


/* Forward declarations for nesting. */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f );

template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f );

template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f );

template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f );
 

/* Base case for traversal: a single input to the transformation function.
 * If called by another overload, which passed f to
 * std::ranges::transform_view, f must be regular-invocable on T.
 */
template< typename T,
          typename F >
    requires std::regular_invocable<F, T>
constexpr auto traverse( const T& input, const F& f )
{
    return std::invoke( f, input );
}

/* This overload was needed to support traversing std::array.  It enforces
 * the C++20 requirements to pass an F to std::ranges::transform_view,
 * except std::regular_invocable, which is checked in the base case.  Will
 * attempt to either construct it from the view or move the elements of the
 * view over.
 */
template< template<class T, std::size_t> class Container,
          typename F,
          typename T,
          std::size_t N >
    requires (!std::regular_invocable<F, Container<T, N>>) &&
             iterable<Container<T, N>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             ( constructible_from_iterators<Container<T, N>> ||
               std::default_initializable<Container<T, N>> )
constexpr auto traverse( const Container<T, N>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, N>;

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );
    if constexpr (std::constructible_from<OutputT,
                                          decltype(results.begin()),
                                          decltype(results.end())>) {
        return OutputT(results.begin(), results.end());
    } else {
        // Sanity check that Container is enough like std::array:
        static_assert( std::default_initializable<OutputT>,
                      "Must be able to create a return object." );
        static_assert( std::output_iterator<typename OutputT::iterator, U>,
                       "The return object must be writable." );

        OutputT output;
        assert(std::size(output) == std::ranges::size(results));

        std::move(results.begin(), results.end(), output.begin());
        return output;
    }
}


/* An overload for functions similar to std::vector<T, Alloc>.  Also emfprces
 * the requirements of std::tanges::transform_view.  Assumes that the allocator
 * is a template class with T as its only parameter.
 */
template< template<class T, class A> class Container,
          typename F,
          typename T,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<T, A<T>>>) &&
             constructible_from_iterators<Container<T, A<T>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<T>, T>
constexpr auto traverse( const Container<T, A<T>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<T>(), f));
    using OutputT = Container<U, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const T& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}


/* An overload similar to the above, intended for containers such as std::set.
 * which take both a comparator and an allocator.  Assumes that both are
 * classes with a single template parameter, the Key.
 */
template< template<class K, class C, class A> class Container,
          typename F,
          typename K,
          template<typename> class C,
          template<typename> class A
        >
    requires (!std::regular_invocable<F, Container<K, C<K>, A<K>>>) &&
             constructible_from_iterators<Container<K, C<K>, A<K>>> &&
             std::copy_constructible<F> &&
             std::is_object_v<F> &&
             allocator<A<K>, K> &&
             comparator<C<K>, K>
constexpr auto traverse( const Container<K, C<K>, A<K>>& input, const F& f )
{
    using U = decltype(traverse(std::declval<K>(), f));
    using OutputT = Container<U, C<U>, A<U>>;
    static_assert(allocator<A<U>, U>, "Could not deduce allocator type.");
    static_assert(comparator<C<U>, U>, "Could not deduce comparator type.");

    const auto results = std::ranges::transform_view(
        std::ranges::subrange{std::begin(input), std::end(input)},
        [&f](const K& x)constexpr{ return traverse(x, f); } );

    return OutputT(results.begin(), results.end());
}
#include <array>
#include <iostream>
#include <set>
#include <stdlib.h>
#include <string.h>
#include <vector>

using std::cerr, std::cout, std::endl, std::size_t;

/* A classic ASCII-only lowercase function, */
constexpr char8_t lowercase(const char c) noexcept
{
    return static_cast<char8_t>(( c >= 'A' && c <= 'Z' ) ?
                                (c + ('a' - 'A')) :
                                c);
}


int main()
{
    { // Test 1:
        static constexpr std::array<unsigned, 5> test_data1 = { 1, 2, 3, 4, 5 };
        static constexpr auto test_result1 = traverse( test_data1,
                                                       [](const unsigned& x)constexpr{ return static_cast<double>(x); }
                                                     );

        for ( size_t i = 0; i < test_result1.size(); ++i ) {
          expect_test(test_data1[i], test_result1[i]);
        }
    }

    { // Test 2:
        static const std::vector<char> test_data2 = {'H','e','l','l','o',',',' ','W','O','R','L','D','!','\0'};
        static const auto test_result2 = traverse( test_data2,
                                                   &lowercase );
        static constexpr char8_t expected2[] = u8"hello, world!";
        
        if ( test_result2.size() == sizeof(expected2) &&
             memcmp( test_result2.data(), expected2, sizeof(expected2) ) == 0 ) {
           cout << "Test 2 passed." << endl;
        } else {
           cout.flush();
           cerr << "Test 2 FAILED with: "
                << reinterpret_cast<const char*>(test_result2.data())
                << endl;
        }
    }

    { // Test 3:
        static const std::set<int> test_data3 = {2,3,4,5,6};
        static const auto test_result3 = traverse( test_data3, std::negate<int>{} );
        static const std::set<int> expected3 = {-2,-3,-4,-5,-6};
 
        if (test_result3 == expected3) {
            cout << "Test 3 passed." << endl;
        } else {
            cerr << "Test 3 FAILED." << endl;
        }
    }

    { // Test 4:
        static const std::vector<std::array<int, 2>> test_data4 =
          { {1, 0}, {0, 1} };
        const auto test_result4 = traverse( test_data4,
                                            [](const std::array<int, 2>& v)constexpr{ return v[0]+v[1]; } 
                                          );
        static const std::vector<int> expected4 = {1, 1};

        if (test_result4 == expected4) {
            cout << "Test 4 passed." << endl;
        } else {
            cerr << "Test 4 FAILED." << endl;
        }
    }

    return EXIT_SUCCESS;
}

And here again is the link to the code on Godbolt.

deleted 105 characters in body
Source Link
Davislor
  • 9.2k
  • 19
  • 39
Loading
added 1553 characters in body
Source Link
Davislor
  • 9.2k
  • 19
  • 39
Loading
Source Link
Davislor
  • 9.2k
  • 19
  • 39
Loading