2

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?

Andrea Allais
  • 447
  • 4
  • 6
  • Why not simply pass a vector of the indices of the `Widget's` you're interested in, along with the reference to the vector? – PaulMcKenzie Nov 10 '20 at 17:44
  • 1
    if you can use C++20 [`std::span`](https://en.cppreference.com/w/cpp/container/span) would be your go to, if not the microsoft Guidelines support library has their own version you can use. – Mgetz Nov 10 '20 at 17:45
  • use iterators. You can apply the same argument that you use in favour of pointers for iterators. If the widgets are actually not stored in a container, one can still supply iterators that when dereferenced yield a reference to the widgets. This can be done in different ways and passing iterators is the most convenient when the widgets are actually stored in a container – 463035818_is_not_an_ai Nov 10 '20 at 17:48
  • If individual `Widget`s are to be passed in without putting them into a container first, then another option is to use a variadic template function, where each `Widget` is passed as its own parameter, ie `Foo(widget1, widget2, ...)`. – Remy Lebeau Nov 10 '20 at 19:23

2 Answers2

1

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});
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • Thanks, this is so beautiful I want to cry. Unfortunately, I don't have ranges in my environment yet, but I guess I can do something similar by passing Foo two iterators and a projector. – Andrea Allais Nov 11 '20 at 16:46
  • @AndreaAllais This one supports C++14: https://github.com/ericniebler/range-v3 It's the library the C++20 ranges are based on. – eerorika Nov 11 '20 at 16:48
0

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.

jhkouy78reu9wx
  • 342
  • 2
  • 8