This is part of why most of the standard library deals in iterators rather than dealing directly with containers themselves. Iterators were designed from the beginning to support iteration over the data in the container, without your having to worry about the container itself.
So in your case, you're passing an std::vector<T>, where T is a std::array<Bunny, 2>, but that's open to change.
If you start with iterators:
template <class It>
auto fun(It begin, It end) {
// This function does no more than reorganizing the `Bunny`s
// which are in `input` in another, more complex way than just
// "a `std::vector` of `std::array`s of 2 `Bunny`s".
// So in my case `auto` is actually a `ResultOfBunnys` or really
// `Result<Bunny>`.
};
...most of what you've discussed simply disappears. There is one point to consider though: how do you get the type the iterator refers to? Fortunately, you're not the first to have run into needing to know that, so the standard library can help. There's an iterator_traits header containing (big surprise) an iterator_traits template that can help you get information about an iterator, without having to know a lot of details. Ultimately, the iterator does have to satisfy its requirements somehow, but it's equipped to deal with the usual variations (e.g,. either a std::vector<T>::iterator or a T *).
So in your case:
template <class It>
auto fun(It begin, It end) {
using value_type = std::iterator_traits<It>::value_type;
using return_type = Result<value_type>;
// define our return value
return_type result;
// do the reorganizing, putting the result into `result`
return result;
};
Some people do find it a bit clumsy to have to pass two parameters instead of one--and I can sympathize with that. If you're sufficiently bothered by that (can can restrict yourself to recent compilers) you may want to look up the new rangeslibrary. Simplifying a lot, this basically lets you take the two iterators, and put them together into a single object, so you only pass one instead of two. In the bigger picture, ranges do a lot more than that, but that's enough for the moment (i.e., enough to deal with any concern over having to pass two parameters instead of one).
If you really need to do something where you need to deal with the container itself rather than just iterating over the data in the container, you may want to look into template template parameters. A template template parameter allows you to pass a template as a parameter to a template. And here I'm talking about the template itself, not an instantiation over a particular type. To use your examples, std::vector<Bunnies> is what I'm referring to as an instantiation over a particular type. In this case, the template itself is just std::vector. So, you can have something like:
template < template<typename, typename> Collection>
class Foo {
...which says that Collection will refer to some template that itself has two template parameters (which most collections do--the contained type, and the Allocator type), so that Foo could deal equally well with an std::vector<T> or an std::list<T> or an std::deque<T>, etc.
#include <vector>
#include <array>
#include <deque>
template <class T>
class Result {
};
// T is some contained type, and Collection is some template that contains T's.
template <typename T, template<typename> typename Collection>
class Foo {
Collection<T> const c;
public:
};
class Bunny {};
int main(){
using Bunnies = std::array<Bunny, 2>;
// A Foo containg a an `std::vector<int>`
Foo<int, std::vector> f;
// A Foo containing an `std::deque<Bunny>`
Foo<Bunny, std::deque> g;
// A Foo containing an `std::list<Bunnies>`
Foo<Bunnies, std::list> h;
// Foo is a template that contains a template...
// so we can create a Foo of Foo--that is, a Foo that contains Foo's.
Foo<Foo<Bunnies, std::list>, std::deque> i;
}