Container agnostic view for data
Hello,
I'd like to create container agnostic view for data, so that I can iterate over all or somehow filtered items. I'd like to be container agnostic, because I use sometimes std::vector, std::list, etc.
Example:
Code:
std::vector<CustomClass> dataVec;
std::list<CustomClass> dataList;
View<CustomClass> viewForVec(dataVec);
View<CustomClass> viewForList(dataList);
for(auto & data: viewForVec) {
doSth();
}
Now, I have no idea how to get it done. Maybe anyone of you could give me some hints about it.
Re: Container agnostic view for data
Well the containers are usually used to store the "arrays"/set of data.
So after you have defined the containers you might want to fill them with data... :rolleyes:
Re: Container agnostic view for data
Quote:
Originally Posted by
VictorN
Well the containers are usually used to store the "arrays"/set of data.
So after you have defined the containers you might want to fill them with data... :rolleyes:
Yeah, you're right. But what if I need to view only several items using some Filter (even number, etc.)
I don't want to copy filtered data, but only store somehow references or iterators to seamlessly iterate over them.
Re: Container agnostic view for data
Then, maybe, something like map will be more suitable for you?
Re: Container agnostic view for data
Quote:
Originally Posted by
miqelm
But what if I need to view only several items using some Filter (even number, etc.)
stl containers expose "container agnostic" views ( or simply views or ranges to use a more established c++ terminology ) via iterators. So, you tipically write a view class ( eg. something storing a lightweight reference to one or more containers ) exposing an iterator, or you just write an iterator and pass ranges ( = iterator pairs ) around. This may or may not be trivial, depending on the view details and on how much general/correct you want this to be.
Otherwise, you use some readymade solution, take a look at the boost iterator/range library for example.
if you need the view to work polymorphically at runtime, you either implement the iterator OOP pattern, or use type erasure.
BTW, note that avoiding copies is not always the right thing to do, performance wise. Sometimes, copying allows to exploit immutability, that can turn out faster down the road ...
Re: Container agnostic view for data
Quote:
Originally Posted by
miqelm
Yeah, you're right. But what if I need to view only several items using some Filter (even number, etc.)
For the filtering you can use a free template function like this,
Code:
template<typename T, typename F, typename S>
void filter(T& target, F lambda, const S& source) {
for (const auto& element : source) {
if (lambda(element)) target.push_back(element);
}
}
This filter function reads elements from the source container. Each element is passed to a lambda expression (a functor) for evaluation. If the lambda returns true the element is written to the target container. Source, target and lambda must share the same element type. The source must have a begin() and an end() function and the target must have a push_back() function. This means filter will work with most STL containers as source, and std::array, std::list, std:: deque and std::string as both target and source. In addition it will work with any other container including one you may write yourself as long as it meets the mentioned requirements.
Here are some lambda expressions. The first will filter out all even CustomClass objects the other will let all slip through,
Code:
auto even = [](const CustomClass& cc) {
return ...; // return true if cc is even;
};
auto any = [](const CustomClass&) {
return true;
};
A minimal container for views could look like below. It will work both as source and target to the filter function. Note that an std:: deque is used for element storage but it could be any data structure. Important is only the interface of View (the signatures of the public functions and their functionality) not how View is implemented. Replacing std:: deque will not affect the usage of View. This is an example of encapsulation.
Code:
template<typename T>
class View {
public:
void push_back(const T& element) {
view.push_back(element);
}
auto begin() const {
return std::begin(view);
}
auto end() const {
return std::end(view);
}
private:
std::deque<T> view;
};
I suggest you introduce a smart pointer such as std::shared_ptr and use it throughout to handle CustomClass objects, both the original data and the views. So once created a CustomClass object is always handled by a smart pointer. Objects are never copied, only smart pointers are. To simplify this usage you could introduce a CustomClass smart pointer type like,
Code:
using CustomClass_Ptr = std::shared_ptr<CustomClass>;
std::vector<CustomClass_Ptr> dataVec;
std::list<CustomClass_Ptr> dataList;
View<CustomClass_Ptr> someView;
There is a convenience function available to create std::shared_ptr objects called std::make_shared. It would typically look like this,
Code:
CustomClass_Ptr cc = std::make_shared<CustomClass>(...); // supply constructor parameters
Make_shared will create the CustomClass object on the heap using new. It is not necessary to ever call delete because destruction happens automagically when an object is no longer pointed at from anywhere (hence smart pointer). Smart pointers were introduced in C++ 11 and have become increasingly popular. They are convenient but slightly more expensive to use than ordinary "raw" pointers. For that reason, if your objects are all very small and plentiful, object copying may still be the more efficient option.
Re: Container agnostic view for data
Continued from my post #6.
I did some recreational programming over the weekend and put it all together in a test program. The purpose has been to achieve "container agnostic" code and as you can see all different containers (vector, list and View) are treated uniformly with very simple means and very few assumptions. Details and motivation are in my previous post.
The code is generic rather than object oriented. Typical of that is that container uniformity is not based on inheritance (of a shared base class) but on ad-hoc sharing of common functions (the push_back, begin and end functions).
Code:
template<typename T, typename F, typename S>
void filter(T& target, F lambda, const S& source) {
for (const auto& element : source) {
if (lambda(element)) target.push_back(element);
}
}
template<typename T>
class View {
public:
void push_back(const T& element) {
view.push_back(element);
}
auto begin() const {
return std::begin(view);
}
auto end() const {
return std::end(view);
}
private:
std::deque<T> view;
};
class CustomClass {
public:
explicit CustomClass(int i) : n(i) {}
int number() const {
return n;
}
private:
int n;
};
using CustomClass_Ptr = std::shared_ptr<CustomClass>;
auto any = [](const CustomClass_Ptr&) {
return true;
};
auto even = [](const CustomClass_Ptr& cc) {
return cc->number()%2 == 0;
};
auto less5 = [](const CustomClass_Ptr& cc) {
return cc->number() < 5;
};
template <typename S>
void print(const S& source) {
for (const auto& cc : source) std::cout << std::to_string(cc->number()) << " ";
std::cout << std::endl;
}
void test() {
std::vector<CustomClass_Ptr> dataVec;
std::list<CustomClass_Ptr> dataList;
View<CustomClass_Ptr> view1, view2, view3;
for (int i=0; i<10; ++i) dataVec.push_back(std::make_shared<CustomClass>(i));
filter(dataList, any, dataVec);
filter(view1, even, dataList);
filter(view2, less5, dataVec);
filter(view3, less5, view1);
print(dataVec);
print(dataList);
print(view1);
print(view2);
print(view3);
}