1

I have a data which is a std::vector of a "small collection" of items of a given type struct Bunny {};.

I was vague about "small collection" because for now it's a collection of two of them, and so I'm just using the following alias

using Bunnies = std::array<Bunny,2>; // probably it'd be better to make it a struct, so that the compiler
                                     // could typecheck, but I think this is not relevant to my question

(Probably using a std::pair<Bunny,Bunny> would be ok too, but I think std::array<…,2> carries with it the idea that the entities must be homogeneous in type.)

That data, std::vector<Bunnies> is the input to a function:

auto fun(std::vector<Bunnies> input) {

    // 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>`.
};

At this point, however, I want to generalize fun, because I'm gonna pass to it not just Bunnys (in the form of a Bunnies), but also other stuff (in the form of some collection).

I guess templates is the way to go for such a generalization. So what could I do? I could do this:

template<typename ObjType>
auto fun(std::vector<std::array<ObjType,2>> input) {

    // do stuff
    // return something
};

but it would hardcode std::array in the function interface. I could do this:

template<typename ObjsType>
auto fun(std::vector<ObjsType> input) {
    using Obj = typename ObjsType::value_type;
    // do stuff
    // return something
};

but this would require that the ObjsType that I pass have a value_type member type. Or I could do this:

template<typename ObjType, typename ObjsType>
auto fun(std::vector<ObjsType> input) {

    // do stuff
    // return something
};

which forces the user to enter a template parameter which is consistent with the one which is deduced via the input argument.

I've been told that the first two options allow less flexibility than the third one. I kind of agree, but I would like to know a bit more about this topic. I don't even know whether there's a book about this things.

21
  • 1
    You first need a clear and concrete idea of what fun does and who will be using it for what. Once you know what fun is all about, only then can you go into asking how to generalize it. You can't generalize things without knowing what you're trying to generalize. Commented Dec 16, 2020 at 22:35
  • @NicolBolas, I've added some comment in fun. Does that help? Regarding the clear and concrete idea (of what fun does, but also in general), don't take me wrong, but if I had one I wouldn't be asking. As regards who will be using it, that's probably the most nasty aspect, because that who is me. So given I still don't have a clear picture of the whole machinery, I risk to make a "working" mess which I end up being not able to handle/maintain.
    – Enlico
    Commented Dec 17, 2020 at 9:07
  • Another thing which I hope helps giving some perspective to my question: I'm an extremely young programmer. I'm enjoying C++, but I tend to answer "let's make it a template" whenever I'm asked to make something work with more than 2 classes. And everytime I'm told that I'm killing flexibility (not because I try to use templates, I feel, but because of the way I try to use them) hence I'm asking this question.
    – Enlico
    Commented Dec 17, 2020 at 9:11
  • I'm don't exclude that probably I need more a discussion than an answer to this question here. I'm just stuck in my understanding of what is wrong in my attempt to use templates. Probably a book would also be good, but I'd like to have a bare understanding before spending money on a book which might end up not being what I need.
    – Enlico
    Commented Dec 17, 2020 at 9:18
  • 1
    Learning SFINAE can also trigger medical (psychological) conditions. Make sure medical and counseling coverage that last longer than employment termination.
    – rwong
    Commented Dec 17, 2020 at 12:28

1 Answer 1

1

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

Also note that when you're dealing with function templates, the compiler can "pull apart" a template into its components (so to speak) so you can do something like this:

template <class T, template<class> class Collection>
Result<T> fun(Collection<T> const &input) {

    return Result<T>(input.front());
}

And when you call it, you just pass a normal collection, and the compiler sorts out which part is the template itself, and which is the type it contains, so you don't have to specify that explicitly:

    std::vector<Bunnies> b { { 0, 1 }, { 1, 2}};
    fun(b); // It will figure out that `Container` is `vector` and T is Bunnies

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.