Proxy --- an indirect container adaptor

Is the following design valid, and if so, would it make a useful addition to the Boost library? I enclose a draft implementation, and look forward to any comments. /Christian - - - It can quite often be of interest to convert a program that uses a direct container into using an indirect container of smart pointers to the objects instead, either just temporarily to see what happens to performance, or permanently if it turns out to be beneficial. Unfortunately, this normally means that the program has to be modified in many different places, even for programs that are not affected as such by the slightly different semantics that indirect containers have. The proxy indirect container header file makes it possible to switch between direct and indirect containers by changing a single typedef in any such program. For example, the following test program was modified to use an indirect vector instead of a direct one by just changing the type declaration from std::vector<person> to proxy_vector<person>::self. //=================================================================== #include <boost/operators.hpp> #include "proxy.hpp" int calls_to_copy_constructor = 0; //=================================================================== class person : boost::totally_ordered<person> //------------------------------------------------------------------- { public: string name; int age; person() {} person(const string& in_name, int in_age); person(const person& other) : name(other.name), age(other.age) { ++calls_to_copy_constructor; } // Compares first name, then age friend bool operator== (const person& a, const person& b); friend bool operator< (const person& a, const person& b); bool read(istream& in_file); }; ostream& operator<< (ostream& out_file, const person& x); //=================================================================== int main (int argc, char* argv[]) //------------------------------------------------------------------- { person a_person; person someone_special("Alice", 27); ifstream in_file("test_proxy.in"); // typedef std::vector<person> buf_type; // <---- typedef proxy_vector<person>::self buf_type; // <---- buf_type buf(5, person("Undefined", -1)); buf.front() = person("First", 1); *(buf.begin() + 1) = person("Second", 2); buf[2] = person("Third", 3); (buf.begin() + 3)->name = "Incomplete"; while (a_person.read(in_file)) { buf.push_back(a_person); } std::sort(buf.begin(), buf.end()); for (buf_type::iterator it = buf.begin(); it != buf.end(); ++it) { cout << *it; if (it->age < 0) cout << " Not a proper age"; if (*it == someone_special) cout << " Someone special"; if (it != buf.begin() && (*it == *(it – 1))) cout << " Duplicate record"; cout << endl; } buf_type::iterator end_it = std::unique(buf.begin(), buf.end()); cout << "\nNumber of unique records: " << (end_it - buf.begin()); cout << "\n\nCalls to 'person's copy constructor: " << calls_to_copy_constructor << endl; return 0; } //=================================================================== So how does this work? First of all, the declaration proxy_vector<T>::self is short for proxy_container< std::vector< proxy<T> > > There are convenience templates like this for the basic container types in STL, but by using the general form the proxy container adaptor can be used on any container that has an STL compliant interface. Class proxy At the very center of the design is the proxy template class. A proxy<T> object is a smart pointer that has a boost::shared_ptr<T> as its only data member. It constructs, assigns, and takes responsibility for deleting the object in the same way as a shared_ptr. It does not support any pointer arithmetic, however, so the ++ and -- operators are undefined. Without pointer arithmetic, there is very little use for the comparison operators as they are defined for shared_ptr, so these are instead given the semantics of element_compare, which means that they compare the two objects as such rather than the pointers. (There is a conversion function to shared_ptr that can be used to test the pointers as such if necessary.) With these definitions, we can use the STL algorithms directly on any Container< proxy<T> >, and they will produce the same results as they would for a Container<T>. This applies both to algorithms like std::sort, which depends on the < operator for comparisons, and algorithms like unique, which relies on the == operator. The proxy class also contains an implicit conversion operator from proxy<T> to T&. This means that we can use a proxy<T> as the actual parameter to any function that expects a T for input or output, or, to put it another way, that the proxy is automatically “cashed in” for a T object as soon as this is required. In particular, this means that we can copy T objects from a Container< proxy<T> > into T variables, in exactly the same way as if it had been a Container<T>, since the proxy<T> object that the iterator delivers will be automatically converted to a T if it is assigned to a T variable. If we assign it to a proxy<T> variable, it will of course remain a proxy. To handle assignments to proxy variables, the default assignment operator is supplemented by operator=(const T&). This operator will create a proxy that points at a new copy of the T object, and assign it to the left hand variable. This means that we now have a Container< proxy<T> > that behaves like a Container<T> for comparisons, and when we retrieve whole objects from the container. Class proxy_iterator To get operator-> working in the same way as for a Container<T>, we define the classes proxy_iterator and const_proxy_iterator. A proxy_iterator is publicly derived from the iterator of the underlying Container< proxy<T> >. It works just like standard iterator in all respects except one, which is that operator-> does a double dereferencing, so that it will deliver a pointer to the T object itself. This lets us use the -> operator in the same way that we would with a direct container. operator* is not changed, however, and retains its single dereferencing semantics. Are we really allowed to define these operators like this? --Yes, we are. While it is true that this means that the semantics will be different for (*x).y and x->y, this does not affect the STL algorithms in any way, since they don’t make any use of the -> operator. (How could they, when they’re supposed to work on the built-in types as well?) The proxy iterators can be mixed freely with the underlying standard iterators, since the derived-to-base-class conversion will convert in one direction, and the proxy iterators have have a conversion constructor that converts in the other direction. Since the compiler will prefer a derived-to-base conversion over a user-defined conversion, the ambiguity problems that are normally associated with having two types that can be implicitly converted in both directions are eliminated. Class proxy_container The class proxy_container, finally, inherits from the underlying Container< proxy<T> > and overrides the definitions for iterator and const_iterator so that the proxy iterators are used instead. It also redefines the standard container member functions that return an iterator so that they return the appropriate proxy iterator. In order to be able to insert T objects directly into the proxy container, the class proxy_container also redefines all members that insert an object into the container so that there are two versions of each function: one that takes a proxy<T> as input and inserts a new proxy that points at the same object, and one that takes a T object and inserts a proxy that points at a newly created copy of the object. Taken together, these classes let us emulate the interface of a Container<T> with something that is actually an indirect container. Since it is only a single type declaration that has to be changed to switch between direct and indirect containers, it becomes practical to do this for testing purposes. The indirect containers still retain the full interface of a traditional container of smart pointers, so the additional capabilities of an indirect container are available in the usual way. - - - I have uploaded the full source code for the draft implementation, as well as and the test program shown here, to the files section at Yahoo, and I have tested it with MSVC6 and gcc 3.3 on Windows XP. For MSVC6 there is the problem that the implementation of std::vector::iterator is just a plain pointer and not a standards compliant iterator, so proxy_vector uses a deque rather than a vector as a workaround. File proxy.hpp: (slightly abridged) //================================================================== // 20-feb-2004 Christian Engström (christian.engstrom@glindra.org) //------------------------------------------------------------------ #include <vector> #include <deque> #include <list> #include <boost/smart_ptr.hpp> //================================================================== // 'proxy' //------------------------------------------------------------------ template <class T> class proxy { private: typedef proxy<T> self; boost::shared_ptr<T> m_sp; public: // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Types typedef T element_type; // "Inherited" from 'boost::shared_ptr' typedef T value_type; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Constructors proxy() {} template<class Y> proxy(const proxy<Y>& other) : m_sp(other.as_shared_ptr()) {} template<class Y> explicit proxy(const boost::shared_ptr<Y>& other) : m_sp(other) {} template<class Y> explicit proxy(const boost::weak_ptr<Y>& other) : m_sp(other) {} template<class Y> explicit proxy(std::auto_ptr<Y>& other) : m_sp(other) {} template<class Y> explicit proxy(Y* other) : m_sp(other) {} // Assignment self& operator=(const self& other) {m_sp = other.m_sp; return *this;} self& operator=(const T& other) {*this = proxy<T>(new T(other)); return *this;} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Conversions operator T&() const {return **this;} // Implicit conversion to T bool is_null() const {return !m_sp;} boost::shared_ptr<T>& as_shared_ptr() {return m_sp;} const boost::shared_ptr<T>& as_shared_ptr() const {return m_sp;} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Comparisons are made on the value, never the pointer. bool operator== (const self& other) const {return **this == *other;} bool operator!= (const self& other) const {return **this != *other;} bool operator< (const self& other) const {return **this < *other;} bool operator> (const self& other) const {return **this > *other;} bool operator<= (const self& other) const {return **this <= *other;} bool operator>= (const self& other) const {return **this >= *other;} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Dereferencing T& operator*() const {return *m_sp;} T* operator->() const {return m_sp.get();} T* get() const {return m_sp.get();} T* as_ptr() const {return m_sp.get();} }; //================================================================== // 'proxy_iterator' //------------------------------------------------------------------ template <class MutableBaseIterator> class proxy_iterator : public MutableBaseIterator { private: typedef proxy_iterator<MutableBaseIterator> self; typedef MutableBaseIterator base; typedef typename base::value_type::element_type T; public: // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Types typedef typename base::difference_type difference_type; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Constructors proxy_iterator() {} proxy_iterator(const MutableBaseIterator& it) : base(it) {} // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Standard operators for iterators T* operator-> () {return (**this).get();} // Indirect self& operator++ () {base::operator++ (); return *this;} self& operator-- () {base::operator-- (); return *this;} self operator++ (int) {return base::operator++ (0);} self operator-- (int) {return base::operator-- (0);} self& operator+= (difference_type n) {base::operator+= (n); return *this;} self& operator-= (difference_type n) {base::operator-= (n); return *this;} self operator+ (difference_type n) const {return base::operator+ (n);} self operator- (difference_type n) const {return base::operator- (n);} friend self operator+ (difference_type n, self x) {return x + n;} friend difference_type operator- (self x, self y) {return base(x) - base(y);} }; //================================================================== // 'const_proxy_iterator' //------------------------------------------------------------------ { // Analagous to proxy_iterator, and with a conversion constructor from proxy_iterator. ... }; //================================================================== // 'proxy_container' //------------------------------------------------------------------ template <class BaseProxyContainer> class proxy_container : public BaseProxyContainer { private: typedef proxy_container self; typedef BaseProxyContainer base; typedef typename BaseProxyContainer::value_type value_type; typedef typename BaseProxyContainer::value_type::element_type T; public: // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Constructors proxy_container() {} proxy_container(const base& c) : base(c) {} template <class InputIterator> proxy_container(InputIterator first, InputIterator last) : base(first, last) {} explicit proxy_container(typename base::size_type n, const value_type& x = value_type()) : base(n, x) {} proxy_container(typename base::size_type n, const T& x) { for (typename base::size_type i = 0; i < n; ++i) { push_back(proxy<T>(new T(x))); } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Iterators redefined typedef proxy_iterator<typename base::iterator> iterator; typedef const_proxy_iterator<typename base::iterator, typename base::const_iterator> const_iterator; typedef std::reverse_iterator<iterator> reverse_iterator; typedef std::reverse_iterator<const_iterator> const_reverse_iterator; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Container members that either return an iterator or take a value_type parameter (or both) iterator begin() {return base::begin();} const_iterator begin() const {return base::begin();} iterator end() {return base::end();} const_iterator end() const {return base::end();} reverse_iterator rbegin() {return base::rbegin();} const_reverse_iterator rbegin() const {return base::rbegin();} reverse_iterator rend() {return base::rend();} const_reverse_iterator rend() const {return base::rend();} void push_front(const value_type& x) {base::push_front(x);} void push_front(const T& x) {base::push_front(proxy<T>(new T(x)));} void push_back(const value_type& x) {base::push_back(x);} void push_back(const T& x) {base::push_back(proxy<T>(new T(x)));} iterator insert(iterator position, const value_type& x) {return base::insert(x);} iterator insert(iterator position, const T& x) {return base::insert(position, proxy<T>(new T(x)));} void insert(iterator position, typename base::size_type n, const value_type& x) {base::insert(position, n, x);} void insert(iterator position, typename base::size_type n, const T& x) ; // Not yet implemented template<typename InputIterator> void insert(iterator position, InputIterator first, InputIterator last) {base::insert(position, first, last);} iterator erase(iterator position) {return base::erase(position);} iterator erase(iterator first, iterator last) {return base::erase(first, last);} void resize(typename base::size_type new_size, const value_type& x) {base::resize(new_size, x);} void resize(typename base::size_type new_size, const T& x) ; // Not yet implemented // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Special list operations void remove(const value_type& value) {base::remove(value);} void remove(const T& value); // Not yet implemented }; //================================================================== // The standard contaiers //------------------------------------------------------------------ template <class T> struct proxy_vector #if !defined(BOOST_MSVC_STD_ITERATOR) {typedef proxy_container< std::vector< proxy<T> > > self;}; #else {typedef proxy_container< std::deque< proxy<T> > > self;}; // MSVC workaround #endif template <class T> struct proxy_deque {typedef proxy_container< std::deque< proxy<T> > > self;}; template <class T> struct proxy_list {typedef proxy_container< std::list< proxy<T> > > self;}; // Associative containers are not yet implemented

On 2/20/04 7:39 AM, "Christian Engström" <christian.engstrom@glindra.org> wrote:
Is the following design valid, and if so, would it make a useful addition to the Boost library? I enclose a draft implementation, and look forward to any comments.
1. I think that someone else has already been working on something like this. (It should be in our YahooGroups files area.) 2. I think that person has done all the containers. (I'm not sure.) More importantly, the other submission is a _lot_ bigger, probably because s/he has thought through more of the issues (or maybe it's over-engineered). 3. I don't remember how the other guy did his/her implementation, but I have a hint anyway. If you're doing an indirect container that elements act like a smart-pointer, that doesn't actually mean that you should actually use a vector< smart_ptr<T> >. I think you should use something like a vector<T *> and handle the smartness yourself to save memory. 4. I know you have an issue with someone else on this list over your iterator/dereferencing policy, but I have an issue with it besides standard compliance. It's not consistent. The "->" gives the final object directly, but the "*" and "[]" only go one step and return a proxy. I don't like this mix. You claim that the directness is not needed for the latter two operators because proxy<T> takes assignments from T and converts to T. If so, then what is the point of using the proxy directly? Can I ever have an use for a "proxy<T>" object as itself? If not, then I don't need to be forced to deal with it and you should change "*" and "[]" to also return T& directly. (Your "proxy<T>" <-> T conversion policy could fail in some template contexts.) -- Daryle Walker Mac, Internet, and Video Game Junkie darylew AT hotmail DOT com

"Daryle Walker" <darylew@hotmail.com> wrote in message news:BC675123.73DC%darylew@hotmail.com... On 2/20/04 7:39 AM, "Christian Engström" <christian.engstrom@glindra.org> wrote:
Is the following design valid, and if so, would it make a useful addition to the Boost library? I enclose a draft implementation, and look forward to any comments.
1. I think that someone else has already been working on something like this. (It should be in our YahooGroups files area.) Perhaps you're think of Thorsten Ottosen's pointer container library? http://groups.yahoo.com/group/boost/files/pointer_container Jonathan

1. I think that someone else has already been working on something like this. (It should be in our YahooGroups files area.)
Perhaps you're think of Thorsten Ottosen's pointer container
"Jonathan Turkanis" <technews@kangaroologic.com> wrote in message news:c1t8bj$q4m$1@sea.gmane.org... library?
My newsreader failed to add >'s to quote Daryle's message. Sorry My comments begin with "Perhaps you're thinking" Jonathan

Daryle Walker wrote:
1. I think that someone else has already been working on something like this. (It should be in our YahooGroups files area.)
Interesting, I'll have a look at that.
[...]
4. I know you have an issue with someone else on this list over your iterator/dereferencing policy, but I have an issue with it besides standard compliance. It's not consistent. The "->" gives the final object directly, but the "*" and "[]" only go one step and return a proxy. I don't like this mix.
Yes, I agree that it's a quite reasonable thing to dislike. Standards issues aside, it violates both reasonable expectations and what is generally considered good taste, so it is certainly not a good design unless it also delivers tangible benefits that can't be achieved in any other way. But I think it does. The "*" operator has to do a normal one-step dereferencing in order for the proxy_container to behave as a normal container of smart pointers. Since we are changing the primary iterators of the container, and not just creating an adapted iterator that is supposed to handle some situations but not other, we can't let the "*" operator do a double dereferencing. If we did, it would essentially cease to be an indirect container since we'd have no way of accessing the smart pointers. For example, when std::sort does assignments and swaps in the course of going about its business, it must do them on the pointers and not on the final objects, or the whole point of using an indirect container is lost. So that's why operator* and operator[] only do a single dereferencing. The "->" operator, on the other hand, doesn't really have very much practical use on a container of smart pointers if it only does single dereferencing. Even if the smart pointer does have some public members, like both shared_ptr and proxy do, those members would normally be of very little interest to the program that is iterating over the container. So since operator-> in this context is mostly unused anyway, why can't we redefine it to provide the final piece of the symmetry with direct containers? The way I see it, having different semantics for "*" and "->" ought to be in the same league as having different semantics for the assignment operator and the constructor for some class --- something that is legal according to the language definition and occasionally useful, but definitely something that should be viewed with a great deal of suspicion. The other aspects of the symmetry between direct and indirect containers still work without this controversial split level dereferencing, so it's actually not as central to the whole package as one might think, but I still think it would be a great pity to have drop it for purely formalistic reasons.
You claim that the directness is not needed for the latter two operators because proxy<T> takes assignments from T and converts to T. If so, then what is the point of using the proxy directly? Can I ever have an use for a "proxy<T>" object as itself?
Yes, absolutely, a proxy<T> is just as useful as a shared_ptr<T> (provided you don't need/want any pointer arithmetic), so it's definitely the intention that they should be useful outside the container as well. For example, a program that already is dealing with proxy<T> or shared_ptr<T> pointers can insert these pointers directly into a proxy_container. The effect then will be the same as when you insert a shared_ptr<T> into a Container< shared_ptr<T> >, that is: the T object won't be copied at all, and the pointer that is stored in the container will refer to the original T. Generally speaking, the intention behind the package is both that it should be easy to switch from direct to indirect containers, and that the resulting container should be a full-fledged container of smart pointers that can be used just like a Container< shared_ptr<T> > can.
[...]
(Your "proxy<T>" <-> T conversion policy could fail in some template contexts.)
Okay, that's something that needs to be investigated further then. /Christian
participants (3)
-
Christian Engström
-
Daryle Walker
-
Jonathan Turkanis