Function taking either reference to single element or a vector

Viewed 122

I want a function that modifies passed in elements of known type, matching against private data we iterate over in an outer loop comparing each to the passed in elements. Quantities are small so no need to build a map or optimize away the n² nature of this. In pseudo-code:

function fill_in_thing_data([in-out] things)
    for (item in private_items)
        info = wrangle(item)
        for (thing in things)
             if (matching(thing, info))
                 thing.data = info.data

Say private_items is expensive to iterator over, or to setup for iteration, and so I definitely want that in the outer loop.

Easy, right? Except that I want to two C++ overloaded functions that share some underlying code, one that takes a non-const reference to thing, one that takes a reference to a vector of things:

void fill_in_thing_data(Thing& single_thing);
void fill_in_thing_data(std::vector<Thing>& some_things);

What's the best way to share code between these 2 functions? I was thinking a helper function that takes iterators or some similar sequence type, and the first function passing in an iterator or sequence made out of the 1 element, the other one made out of the vector. I want to use C++ idioms so was looking at doing it with ForwardIterators.

Problem:

  • C++ iterators in general aren't easy. Ok using them is ok, but writing a function that takes any kind of ForwardIterator to simply a known type seems unexpectedly tricky.
  • Is a special case iterator over a single element reference something that's available? Or something that's easy to define? It seems the answers are no and no.

Instead of iterators, this is my ugly solution using a visitor lambda passed in, and a visit lambda passed back, allowing the iteration logic to be abstracted out of the shared function:

using VisitThing = std::function<bool(Thing& thing)>;
using ThingVisitor = std::function<bool(VisitThing visit)>;

void match_things(ThingVisitor visitor);
void fill_in_thing_data(Thing& single_thing) {
    match_things([&](VisitThing visit) {
        visit(single_thing);
    });
}
void fill_in_thing_data(std::vector<Thing>& some_things) {
    match_things([&](VisitThing visit) {
        for (auto& thing : some_things) { visit(thing); }
    });
}

void match_things(ThingVisitor visitor) {
    auto stuff = fetch_private_stuff()
    while (item = stuff.get_next_item()) { // can't change this home-brew iteration
        auto item_info = wrangle(item) // but more complex, logic about skipping items etc
        visitor([&](Thing& thing) {
            if (item_info.token == thing.token)) {
                thing.data = item_info.data;
            }
        }
    }
}

Am I wrong and can do this with iterators without too much complexity? Or can I do this better in some other way, like maybe a good data structure class that can either be built with either the reference or the vector and then pass that in? Or like something else obvious I'm just not seeing? Thanks!

1 Answers

I hope I got OPs issue right. To me, it boiled down
to have a function which can be applied

  • to a single reference as well as
  • to a std::vector of instances.

There is actually a very simple solution which I even learnt (decades ago) in C but would work in C++ as well:

The function takes a pointer and a count:

void fill(Thing *pThing, size_t len);

To use it with a single instance:

Thing thing;
fill(&thing, 1);

To use it with a std::vector<Thing>:

std::vector<Thing> things;
fill(&things[0], things.size());

IMHO, this is somehow C-ish, beside of the fact, that OP mentioned iterators.

So, here we go:

template <typename ITER>
void fill(ITER first, ITER last)
{
  for (const Item &item : items) {
    for (ITER iter = first; iter != last; ++iter) {
      if (matching(*iter, item)) iter->data = item;
    }
  }
} 

// a wrapper for a single thing
void fill(Thing &thing) { fill(&thing, &thing + 1); }

// a wrapper for a vector of things
void fill(std::vector<Thing> &things) { fill(things.begin(), things.end()); }

The principle is still the same like above but using iterators.

A complete demo:

#include <iostream>
#include <vector>

// an item
struct Item {
  int id = 0;
};

// the vector of OPs private items
std::vector<Item> items = {
  { 1 }, { 2 }, { 3 }
};

// a thing
struct Thing {
  int id;
  Item data;
  
  Thing(int id): id(id) { }
};

// a hypothetical function matching a thing with an item
bool matching(const Thing &thing, const Item &item)
{
  return thing.id == item.id;
}

// a generic fill (with matches) using iterators
template <typename ITER>
void fill(ITER first, ITER last)
{
  for (const Item &item : items) {
    for (ITER iter = first; iter != last; ++iter) {
      if (matching(*iter, item)) iter->data = item;
    }
  }
} 

// a wrapper for a single thing
void fill(Thing &thing) { fill(&thing, &thing + 1); }

// a wrapper for a vector of things
void fill(std::vector<Thing> &things) { fill(things.begin(), things.end()); }

// overloaded output operator for thing (demo sugar)
std::ostream& operator<<(std::ostream &out, const Thing &thing)
{
  return out << "Thing { id: " << thing.id
    << ", data: Item { id: " << thing.data.id << "} }";
}

// demo sugar
#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ 

// demonstrate
int main()
{
  // call to fill a single instance of Thing
  DEBUG(Thing thing(2));
  DEBUG(std::cout << thing << '\n');
  DEBUG(fill(thing));
  DEBUG(std::cout << thing << '\n');
  std::cout << '\n';
  // call to fill a vector of Thing
  DEBUG(std::vector<Thing> things = { Thing(2), Thing(3) });
  DEBUG(for (const Thing &thing : things) std::cout << thing << '\n');
  DEBUG(fill(things));
  DEBUG(for (const Thing &thing : things) std::cout << thing << '\n');
}

Output:

Thing thing(2);
std::cout << thing << '\n';
Thing { id: 2, data: Item { id: 0} }
fill(thing);
std::cout << thing << '\n';
Thing { id: 2, data: Item { id: 2} }

std::vector<Thing> things = { Thing(2), Thing(3) };
for (const Thing &thing : things) std::cout << thing << '\n';
Thing { id: 2, data: Item { id: 0} }
Thing { id: 3, data: Item { id: 0} }
fill(things);
for (const Thing &thing : things) std::cout << thing << '\n';
Thing { id: 2, data: Item { id: 2} }
Thing { id: 3, data: Item { id: 3} }

Live demo on coliru


A note about the template function with iterators:

Just recently I became painfully aware that just naming something ITER is not enough to grant that it accepts iterators only. In my case, I had a variety of overloads, and the one accepting an iterator range was only one of them. So, I had to manage to eliminate ambiguities. For this, I found a quite simple solution (here in Stack Overflow) using SFINAE:

template <typename ITER,
  typename = decltype(
    *std::declval<ITER&>(), void(), // has dereference
    ++std::declval<ITER&>(), void())> // has prefix inc.
void fill(ITER first, ITER last);

An iterator is something which (among other things) has to provide a de-reference and an increment operator. The 2nd template type argument checks precisely this in its default initialization. (Template type arguments for SFINAE should never be used explicitly in template instances, of course.)

Live demo on coliru

Related