Pass dynamic array of objects safely and without forcing copies

Viewed 85

I want to define a function Foo that takes as argument a few objects Widget, whose number is only known at run time. The simplest signature would be:

void Foo(const std::vector<Widget>& widgets);

The function can be made more general by turning into a template and using iterators. However, if the Widgets are not stored in some contiguous form, any signature based on array-of-values would force the caller to make a copy of each. For example, a copy is necessary if each Widget is contained in some other object Holder, or if Foo needs to be called on just 3 widgets that come from idiosyncratic sources.

Copies can be avoided if the function accepts a vector of pointers:

void Foo(const std::vector<const Widget*>& widgets);

but this makes the function unsafe. For example in:

std::vector<const Widget*> widgets;
for (const auto& holder : holders) {
   widgets.push_back(&holder.get_widget());
}
Foo(widgets);

forgetting the ampersand on const auto& would lead to segfault.

I can think of a couple of solutions that are both safe and copy-free, involving lambdas or a Foo template and custom iterators, but they are quite gross. What would be the best design for this situation?

2 Answers

The function can be made more general by turning into a template and using iterators.

This is often a good design. Or alternatively, accept a generic range.

However, if the Widgets are not stored in some contiguous form, any signature based on array-of-values would force the caller to make a copy of each.

Which is a good reason to use iterator / generic ranges instead of a hardcoded vector type.

forgetting the ampersand on const auto& would lead to segfault.

Only if you're lucky. A segfault is not guaranteed. Indeed, the programmer must be careful when indirection is involved. Another problem with vector of iterators is the unnecessary overhead of creating a new vector.


Example with ranges:

void Foo(const auto& widgets);

auto holder_to_widget = [](const auto& holder) {
    return holder.get_widget();
};
Foo(holders | ranges::transform_view{holder_to_widget});

Iterators are the way to go:

template<typename It>
void Foo(It first, It last)
{
    for (; first != last; ++first)
    {
        // do work with *first
    }
}

// contiguous storage 
std::vector<Widget> widgets;
// populate vector here, then call Foo
Foo(widgets.cbegin(), widgets.cend());

// plain array
auto widgets = std::make_unique<Widget[]>(count);
// assign values to array elements here, then call Foo
Foo(widgets.get(), widgets.get() + count);

// non-contiguous storage
std::list<Widget> widgets;
// populate list here, then call Foo
Foo(widgets.cbegin(), widgets.cend());

You can write custom iterator class for your custom storage for Widget objects. See How to implement an STL-style iterator and avoid common pitfalls? and the likes for tips on iterator design. Basically, Foo above requires only operator*, operator!= and operator++ to be defined. However, other operators might be requred to be defined depending on operations performed by Foo.

Related