Search code examples
c++c++11containersc++14encapsulation

Standard containers encapsulation and range-based for loops


I'm designing a class which has two standard vectors as members. I would like to be able to use range-based for loops on the vector elements and I came up with this solution

#include <iostream>
#include <vector>

using namespace std;

class MyClass {
public:
  void addValue1(int val){data1_.push_back(val);}
  void addValue2(int val){data2_.push_back(val);}
  vector<int> const & data1() const {return data1_;}
  vector<int> const & data2() const {return data2_;}
  // ...
private:
  vector<int> data1_;
  vector<int> data2_;
  // ...
};

void print1(MyClass const & mc) {
  for (auto val : mc.data1()){
    cout << val << endl;
  }
}

void print2(MyClass const & mc) {
  for (auto val : mc.data2()){
    cout << val << endl;
  }
}

int main(){
  MyClass mc;
  mc.addValue1(1);
  mc.addValue1(2);
  mc.addValue1(3);
  print1(mc);
}

Clearly, the alternative of defining begin() and end() functions doesn't make sense since I have two distinct vectors.

I would like to ask the following questions:

  • A shortcoming of the proposed solution is that the contents of the two vectors cannot be changed (due to the const qualifier). In the case I need to modify the vector elements how can I modify the code? EDIT: the modification should preserve encapsulation

  • Considering data encapsulation, do you think it is bad practice to return a (const) reference to the two vectors?


Solution

  • Use something like gsl::span<int> and gsl::span<const int>.

    Here is a minimal one:

    template<class T>
    struct span {
      T* b = 0; T* e = 0;
      T* begin() const { return b; }
      T* end() const { return e; }
      span( T* s, T* f ):b(s),e(f) {}
      span( T* s, std::size_t len ):span(s, s+len) {}
      template<std::size_t N>
      span( T(&arr)[N] ):span(arr, N) {}
      // todo: ctor from containers with .data() and .size()
    
      // useful helpers:
      std::size_t size() const { return end()-begin(); }
      bool empty() const { return size()==0; }
      T& operator[](std::size_t i) const { return begin()[i]; }
      T& front() const { return *begin(); }
      T& back() const { return *(std::prev(end())); }
    
      // I like explicit defaults of these:
      span() = default;
      span(span const&) = default;
      span& operator=(span const&) = default;
      ~span() = default;
    };
    

    now you can write:

    span<int const> data1() const {return {data1_.data(), data1_.size()};}
    span<int const> data2() const {data2_.data(), data2_.size()};}
    span<int> data1() {return {data1_.data(), data1_.size()};}
    span<int> data2() {data2_.data(), data2_.size()};}