8

I would like to hide a vector field in my class but allow easy iteration through its elements but nothing else. So that class's client would be able to do

for (auto element : foo.getElements()) { }

but not

foo.getElements()[42];

Is there some simple way of achieving this w/o creating new confusing types?

Justin
  • 24,288
  • 12
  • 92
  • 142
MK.
  • 33,605
  • 18
  • 74
  • 111
  • wait... can I just make getElements return std::iterator? – MK. Jun 11 '18 at 17:49
  • No, it's not that easy :) – Rakete1111 Jun 11 '18 at 17:49
  • 1
    Does a [span](https://stackoverflow.com/q/45723819/1896169) do what you want? You'd still be able to write `foo.getElements()[42]`, but it doesn't expose that the underlying container is a vector. – Justin Jun 11 '18 at 17:51
  • no, I need to prohibit bracket operator. Do some bounds checking. – MK. Jun 11 '18 at 17:53
  • @Rakete1111 why not? – MK. Jun 11 '18 at 17:53
  • 5
    @MK. For a ranged for loop you need to return a type with an accessible `begin` and `end` function. `std::iterator` doesn't have one. – Rakete1111 Jun 11 '18 at 17:55
  • 5
    Define "confusing". – T.C. Jun 11 '18 at 17:57
  • There's no off-the-shelf type in the standard with these restrictions, although creating such a type should be trivial. – Drew Dormann Jun 11 '18 at 17:57
  • The title has the design issue inside-out. The goal here is to create a container whose iterators have a particular set of properties -- iteration but not assignment or indexing seems to be most of the goal. That describes a const forward iterator or a const bidirectional iterator, depending on whether reverse iteration should be allowed. `std::vector` is a tool; the goal isn't to encapsulate it, but to use it to implement the internals of this container class. So do it: create a class that uses a vector to hold its data, and defines its own iterator type for `begin()` and `end()` to return. – Pete Becker Jun 11 '18 at 18:12
  • What does `std::iterator` have to do with the issue at hand? – AnT stands with Russia Jun 11 '18 at 18:28
  • why isn't that an XY question? – aaaaa says reinstate Monica Jun 11 '18 at 22:28
  • Why don't you want `foo.getElements()[42]` to work? I'd recommend `const std::vector&` if it wasn't for that. – Mooing Duck Jun 11 '18 at 23:08
  • What's the point of this? If someone wants `[42]` they can do it by iterating 43 times. You're just forcing them to use an O(n) algorithm when they could use O(1). – Barmar Jun 11 '18 at 23:52
  • @Barmar I am giving a user a safe way to access by index. [42] fails (or might even be UB?) if there are 37 elements in the array. It is not a safe API to expose. – MK. Jun 12 '18 at 02:54
  • @aaaaaa I'm dealing with a really old poorly written code base. I need to make the API safer without rewriting all if its users. The most common usage patter is iterating through all the elements of this vector which is perhaps not beautiful, but safe, so I want to preserve it. Accessing by index is not safe, so I want to replace it with a getter which returns std::optional. So sure, it is a bit of an XY problem but perhaps justifiably. – MK. Jun 12 '18 at 13:54
  • thanks for context, @MK – aaaaa says reinstate Monica Jun 12 '18 at 15:24

4 Answers4

14

I cannot say what is and is not a "new confusing type". But this is sufficient for the needs of a range-based for:

template<typename Iter>
class iterator_range
{
public:
  iterator_range(Iter beg, Iter end) : beg_(beg), end_(end) {}

  Iter begin() const {return beg_;}
  Iter end() const {return end_;}

private:
  Iter beg_, end_;
};

The Range TS adds more complexity to what constitutes a "range", but this is good enough for range-based for. So your foo.getElements function would look like this:

auto getElements()
{
  return iterator_range<vector<T>::iterator>(vec.begin(), vec.end());
}

auto getElements() const
{
  return iterator_range<vector<T>::const_iterator>(vec.begin(), vec.end());
};
Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Uh this violates the "no confusing types" thing and doesn't have higher order functions. But it is kind of the simplest, I suspect I'll have to go with this. – MK. Jun 11 '18 at 19:37
  • 8
    @MK.: "confusing types" is in the eye of the beholder. To me, there's absolutely nothing "confusing" about an iterator range type. And you said *nothing* about higher-order functions. – Nicol Bolas Jun 11 '18 at 19:48
  • oh yeah totally. I'm just trying to compare your (great) answer to Vittorio's (also great) answer. Thanks! – MK. Jun 11 '18 at 19:52
  • Your `iterator_range` does satisfy the Ranges TS `Range` concept. – Barry Jun 12 '18 at 15:24
  • sigh. You can still do iter_range.begin()[42]. This makes me sad. – MK. Jun 21 '18 at 13:39
  • @MK.: So what? That's part of the functionality of a random access iterator. And even with a forward iterator, you could do `std::advance(iter_range.begin(), 42)` to accomplish the same effect. – Nicol Bolas Jun 21 '18 at 13:40
  • Well, my goal is to have compiler ensure safe usage of the class. And for that I wanted to _prohibit_ direct indexing. – MK. Jun 21 '18 at 17:59
  • @MK.: C++ is not a safe language; its iterator model is not inherently safe. You can always walk off the bounds of an iterator-based range, and there's nothing the compiler can do to prevent that. – Nicol Bolas Jun 21 '18 at 18:19
5

You can use an higher-order function to only expose iteration functionality:

class something
{
private:
    std::vector<item> _items;

public:
    template <typename F>
    void for_items(F&& f)
    {
        for(auto& i : _items) f(i);
    }
};

Usage:

something x;
x.for_items([](auto& item){ /* ... */ });

The advantages of this pattern are:

  • Simple to implement (no need for any "proxy" class);
  • Can transparently change std::vector to something else without breaking the user.

To be completely correct and pedantic, you have to expose three different ref-qualified versions of for_items. E.g.:

template <typename F>
void for_items(F&& f) &      { for(auto& i : items) f(i); }

template <typename F>
void for_items(F&& f) const& { for(const auto& i : items) f(i); }

template <typename F>
void for_items(F&& f) &&     { for(auto& i : items) f(std::move(i)); }

The above code ensures const-correctness and allows elements to be moved when the something instance is a temporary.

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • 2
    @Rakete1111: That's fine; it's not a proper container anyway ;) – Nicol Bolas Jun 11 '18 at 18:19
  • This is probably the best (most modern) answer but I'm accepting the other one because that's what I ended up doing to keep code changes (for consumers) to a minimum. – MK. Jun 11 '18 at 22:04
3

Here is a proxy-based approach (though I'm not sure whether the new type meets the requirement of not being confusing).

template<class Container> class IterateOnlyProxy {
    public:
        IterateOnlyProxy(Container& c) : c(c) {}

        typename Container::iterator begin() { return c.begin(); }
        typename Container::iterator end() { return c.end(); }

    private:
        Container& c;
};

The proxy is used as a return type for the getElements() method,

class Foo {
    public:
        using Vec = std::vector<int>;
        using Proxy = IterateOnlyProxy<Vec>;

        Proxy& getElements() { return elementsProxy; }

    private:
        Vec elements{4, 5, 6, 7};
        Proxy elementsProxy{elements};
};

and client code can iterate over the underlying container, but that's about it.

Foo foo;

for (auto element : foo.getElements())
    std::cout << element << std::endl;

foo.getElements()[42]; // error: no match for ‘operator[]’
lubgr
  • 37,368
  • 3
  • 66
  • 117
0

If you want to hide a vector field in your class but still do range based for-loop, you could add your own iterator based on vector::iterator.

A simple (and incomplete) example could be like:

#include <iostream>
#include <vector>

class Foo
{
    public:
    class iterator
    {
        public:
        iterator(std::vector<int>::iterator n) : p(n) {}
        bool operator==(iterator& rhs) { return p == rhs.p; }
        bool operator!=(iterator& rhs) { return p != rhs.p; }
        iterator& operator++() { p++; return *this; }
        int& operator*() { return *p; }

        private:
        std::vector<int>::iterator p;
    };

    iterator begin() { return iterator(v.begin()); }
    iterator end() { return iterator(v.end()); }

    private:
    std::vector<int> v {1, 2, 3, 4, 5};
};

int main() {
    Foo foo;
    for(auto y : foo) std::cout << y << std::endl; 
    return 0;
}
Support Ukraine
  • 42,271
  • 4
  • 38
  • 63