Handling non-default-constructible containers
You are currently assuming that once you have determined the type of a container using recursive_invoke_result_t<>, that you can default-construct a new container. However, that might not be the case; for example, if an allocator is used that doesn't have a default constructor.
Most STL containers have a get_allocator() member function that will return the allocater object that was used to construct it. You can pass that to the container you want to construct. Of course, that means an increase in complexity of your code. Ideally, you would create an out-of-class function that can create an empty copy of any container, so that you can write:
template<std::size_t unwrap_level = 1, class T, class F>
requires (unwrap_level <= recursive_depth<T>())
constexpr auto recursive_transform(const T& input, const F& f)
{
if constexpr (unwrap_level > 0)
{
auto output = clone_empty_container(input);
std::ranges::transform(
input,
std::inserter(output, std::ranges::end(output)),
[&f](auto&& element) { return recursive_transform<unwrap_level - 1>(element, f); }
);
return output;
}
else
{
return std::invoke(f, input);
}
}
You could even consider having it handle comparator and hash objects passed to containers like std::map and std::unordered_map, call reserve() based on the size of the input, handle container adapters like std::stack() that take a container as a parameter in the constructor, and maybe even upcoming types like std::flat_set.