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.

Enlico
  • 131
  • 7
  • 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. – Nicol Bolas Dec 16 '20 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 Dec 17 '20 at 09: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 Dec 17 '20 at 09: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](https://stackoverflow.com/questions/622659/what-are-the-good-and-bad-points-of-c-templates), 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 Dec 17 '20 at 09:18
  • Will `fun` always operate on a collection of things that are collected in groups of 2? – jxh Dec 17 '20 at 09:18
  • @jxh, for now I'd answer yes. – Enlico Dec 17 '20 at 09:18
  • For the groups of 2, what can you offer to generically obtain the first and second thing? – jxh Dec 17 '20 at 09:19
  • The codebase has a `left` and `right` function objects that work pretty reliably, as far as I can tell. – Enlico Dec 17 '20 at 09:31
  • `std::array` does not offer such an interface. If you can assume any group of 2 inside the collection will be interpreted properly by your `left` and `right` functors, then you can use `decltype` to detect the type returned by them. – jxh Dec 17 '20 at 09:52
  • _As far as I can tell_ = I've used those `left`/`right` on a `std::array` and they operate like `[0]` and `[1]`. So you mean `decltype(left(one of those arrays))`? – Enlico Dec 17 '20 at 09:55
  • Yes, that should do it. – jxh Dec 17 '20 at 11:01
  • @Enlico The two C++ topics that you might want to learn are: `decltype` and ["SFINAE"](https://en.cppreference.com/w/cpp/language/sfinae). Note: learning SFINAE will disqualify a C++ programmer from employment because programming productivity will drop to zero during the first few years of learning SFINAE. Most people eventually abandon C++ altogether after they started learning SFINAE seriously. – rwong Dec 17 '20 at 12:21
  • Large corporations (over 10000 headcount) that use C++ significantly have SFINAE working groups that create SFINAE-based C++ library features for internal use. They typically also participate in C++ language specification committees. Each new version of C++11, C++14, C++17 introduces significant and sometimes breaking changes to SFINAE, which are meant to make SFINAE less insane (but which also breaks archaic insane SFINAE usage). Also, SFINAE users can encounter compiler differences and bugs (vendor and version specific) that ordinary users won't see. – rwong Dec 17 '20 at 12:27
  • 1
    Learning SFINAE can also trigger medical (psychological) conditions. Make sure medical and counseling coverage that last longer than employment termination. – rwong Dec 17 '20 at 12:28
  • @rwong, I know what `decltype` does and I know what SFINAE is, and as regards how to _use_ SFINAE, I'm still at the beginning, but I could at least understand and put in practice a colleague's suggestion of _you can SFINAE that out_ when I was having a clash between two friend functions. This is just to say, I'm eager to go deep in SFINAE, templates, `enable_if`/`void_t`, and all that stuff. As regards medical conditions, I'm proud to say that I'm on my way already, if that puts me in a better position to learn SFINAE. – Enlico Dec 17 '20 at 12:35
  • However, @rwong, I'm pretty sure that my problem with what I'm trying to understand with this question is not about SFINAE or `decltype`, which would "just" be _tools_ to accomplish some useful/good/flexible/whatever design; it's about the fact that I lack some theoretical education/training about code design, probably. And probably these networks are not the right place to learn that :( – Enlico Dec 17 '20 at 12:38
  • @Enlico My personal experience is that (since I'm a C++ and C# dual user), eventually, sane C++ code design should take lessons from C#, and learn to use vtable (C++ flavor of OOP) more. To begin with, create an abstract interface that mimics `ICollection` and `IList` and `Tuple`, and create adapters for specific implementations of C++ containers. – rwong Dec 17 '20 at 13:03
  • C++ iterators is both right and wrong; right because it is indeed a mechanism that can work over wide range of data structures; wrong because each data structure has its own iterator types; it's not possible to have polymorphism over them. The solution is simple - just wrap around them, OOP style. – rwong Dec 17 '20 at 13:06
  • If a C++ type is a kind of a container such that it has a `begin()` method that returns an iterator that can be dereferenced into something, you can take it as a way to decipher the "element type" of that container. It may be necessary to sprinkle some [`std::remove_cvref`](https://en.cppreference.com/w/cpp/types/remove_cvref). Note: in C++, for some containers, `begin()` and `end()` can have different return types. – rwong Dec 17 '20 at 13:09
  • Because `end()` just needs to return a kind of guard, not a true iterator, right? As regards vtables, inheritance, and whatever is dynamic/run-time when it could be static/compile-time, I feel that experienced C++ programmers (at least those I know) tend to be at least not happy about using it, unless where it is stricly necessary. – Enlico Dec 17 '20 at 13:27
  • The `decltype` suggestion was to remove your last two objections in your question. – jxh Dec 17 '20 at 21:57

1 Answers1

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
Jerry Coffin
  • 44,385
  • 5
  • 89
  • 162