Elegant Class Solution for Data Buffers

Started by
10 comments, last by seanw 19 years, 7 months ago
Data Buffers. We fight against the potential problems, buffer overrun, forgeting to delete[] the buffer, forgeting to clear the buffer area, etc... So, I'm trying to design a class that would encapsulate and minimize the occurrance of those problems. The old way would is: char *buffer = new char[30000]; Under the new class, we would write: cDBuffer< char > buffer(30000); Under this new templated class i have the following variables: - size - Size in bytes of the buffer - elements - number of elements in the buffer - used - how much of the buffer is actually in use - element_size - size of each element Now, let me explain the concept of elements. If i have a buffer made of 100 char, then what size is it? 100 bytes of course. But what if I have a buffer made of 100 sVertex? struct sVertex { float x,y,z; }; // 12 bytes So, under this code, the variables would take the following values: cDBuffer< sVertex > Vert(100); - size: 1200 bytes - elements: 100 - used: 1200 - element_size: 12 bytes Now, for the methods: - Constructor/ Destructor - Resize - Clear - "=" operator overload (a simple memcpy from another buffer) Now comes my problem, see this: sVertex v[100]; v[2].x = 30; And in the buffer cDBuffer< sVertex > v(100); So, now, what operators do I have to overload so that i can access "x" of the 2nd element? Something along the lines of: v[2].x = 30; or v[2]->x = 30; Can it be done?
Advertisement
How, exactly, does your idea offer any advantages over std::vector?
Quote:Original post by Miserable
How, exactly, does your idea offer any advantages over std::vector?


Who said it did?! Im developing my own code here, to learn...
You need to overload operator [] to return a reference to the type, like so:
template<typename T>class Buffer{...   T *m_data;   size_t m_size;public:   T& operator[] (size_t index)   {      if(index >= m_size)         throw OUT_OF_BOUNDS_EXCEPTION;      return m_data[index];   }   const T& operator[] (size_t index) const   {      if(index >= m_size)         throw OUT_OF_BOUNDS_EXCEPTION;      return m_data[index];   }};
"Walk not the trodden path, for it has borne it's burden." -John, Flying Monk
Fair enough, as long as you are aware that for production code, there is a better solution available. You'll probably want to overload operator[]() and operator[]() const (with proper argument types, e.g. int) to return a value type reference and const reference, respectively.
Modelling a buffer type on std::vector would be horriably inefficient.

first of all i would have assumed that a buffer does not dynamically grow in size like a vector.

Secondly when a vector is constructed with a specified size it also initializes all elements, you don't wont that kind of behaviour for a buffer.

A better design would be a buffer that on construction takes a size of the buffer and then allocates a chunk of uninitialized memory of that size and doesn't not change. Its sort of like constructing a vector with no size specified and then calling reserve which grabs a chunk of uninitialized memory.

So you'll wont to paramterize your buffer type by an allocator type just like STL containers, then you maintain these invariants:

position <= limit <= capacity

where:

A buffer's capacity is the number of elements it contains. The capacity of a buffer is never negative and never changes.

A buffer's limit is the index of the first element that should not be read or written. A buffer's limit is never negative and is never greater than its capacity.

A buffer's position is the index of the next element to be read or written. A buffer's position is never negative and is never greater than its limit.

These could be just pointers/iterators into the buffer, or just integer indices, between begin and (position - 1) elements have been initialized and between capacity - position is unintialized.

Also the kind of operations you'll wont for a buffer are not going to be the same as a vector.

[Edited by - snk_kid on September 25, 2004 3:20:09 AM]
Quote:Original post by snk_kid
Modelling a buffer type on std::vector would be horriably inefficient.

Depends on how you use it.
Quote:first of all i would have assumed that a buffer does not dynamically grow in size like a vector.

Use reserve, or pre-declare vector's size on construction. Granted, there's some performance hit allocating and deallocating - it depends on how you use it. Making it static will screw up multithreading (unless you add a "thread-local" flag), but would solve your problems with alloc/dealloc.
Quote:Secondly when a vector is constructed with a specified size it also initializes all elements, you don't wont that kind of behaviour for a buffer.

Depends. Use reserve() and pop_back at initialization or something.
Quote:A better design would be a buffer that on construction takes a size of the buffer and then allocates a chunk of uninitialized memory of that size and doesn't not change.

If it's not going to change size, why not just make the size of the array a template argument, and never allocate it, instead having it on the stack?
Quote:Its sort of like constructing a vector with no size specified and then calling reserve which grabs a chunk of uninitialized memory.

Only with no possibility to change your mind later, I should note. Note that he does mention a Resize() member.
Quote:Original post by MaulingMonkey
Quote:Original post by snk_kid
Modelling a buffer type on std::vector would be horriably inefficient.

Depends on how you use it.


A buffer is not a vector, sure you can always use a vector to behaviour like a buffer but i'm talking about normal/typical usage, with-out using reserve.

Quote:Original post by MaulingMonkey
Quote:first of all i would have assumed that a buffer does not dynamically grow in size like a vector.

Use reserve, or pre-declare vector's size on construction. Granted, there's some performance hit allocating and deallocating - it depends on how you use it.


You've just proved that designing a buffer type based vector is a bit of silly idea.

Quote:Original post by MaulingMonkey
Making it static will screw up multithreading (unless you add a "thread-local" flag), but would solve your problems with alloc/dealloc.


Making it static doesn't solve any problems, its to inflexible, and limits the usage to just one memory model, best thing about making it parameterized by allocator type you can use any memory model you wont.

Quote:Original post by MaulingMonkey
Quote:Secondly when a vector is constructed with a specified size it also initializes all elements, you don't wont that kind of behaviour for a buffer.

Depends. Use reserve() and pop_back at initialization or something.


why would you wont to pop_back for or did you mean push_back?

anyways so why not just design it to work like that in the first place, give a choice have a constructor acts like reserve, instead of constructing an empty buffer and then calling reserve and then pushing back being the only choice.

Quote:Original post by MaulingMonkey
Quote:A better design would be a buffer that on construction takes a size of the buffer and then allocates a chunk of uninitialized memory of that size and doesn't not change.

If it's not going to change size, why not just make the size of the array a template argument, and never allocate it, instead having it on the stack?


Again its to inflexible & restrictive, what happens if you don't know the size until run-time?

also if you use it with another user-defined type you must specifiy its size with-in the class you can-not specifiy the size at construction of the other user-defined type, i mean:

template < typename T, size_t n > class buffer { /* ... */ };class foo {    typedef unsigned char byte;    buffer< byte, 1000 > buff;                           ^public:                    |                           |    foo(size_t n); //<------ not cleanly possible};


[Edited by - snk_kid on September 25, 2004 5:10:26 AM]
What benefits are there to storing the size in bytes of the buffer? Or the element size? You can always calculate that if its needed (for memcpy() or whatnot).

In my limited experience I find that there is definitely more than one type of buffer needed. There is need for both a fixed size buffer that lives on the stack (either through templates or that stack allocation function whose name evades me at the moment). What the OP is describing is merely a collection-type buffer...I'd say that buffer is entirely the wrong word here.

Quote:People that say "Google is you friend" are either too busy to give a complete answer, or simply dont know one.

All I gotta say is...hahaha.
--God has paid us the intolerable compliment of loving us, in the deepest, most tragic, most inexorable sense.- C.S. Lewis
Here is roughly an idea of how to write some kind of buffer class well its something to start on or think about, it's modelled mostly around java.nio's buffer types, implementated some of it but i don't have time to complete it, the base buffer is not mean't to be used by clients it just implements RAII to make exception safe code easier.

things that are lacking apart from a full implemenation, exceptions for put/get methods, probably wont to write proper const_iterator & iterator types for it so it has a iterator_category etc, and i probably missed something else aswell.

#ifndef _BUFFERN_#define _BUFFERN_#include <cstddef>#include <exception>#include <stdexcept>#include <memory>#include <algorithm>namespace utility {	template < typename _Tp, typename _Alloc >	class buffer;	template < typename _Tp, typename _Alloc >    bool operator==(const buffer< _Tp, _Alloc >&, const buffer< _Tp, _Alloc >&);    	template < typename _Tp, typename _Alloc >    bool operator!=(const buffer< _Tp, _Alloc >&, const buffer< _Tp, _Alloc >&);	//Relative get method	template < typename _Tp, typename _Vp, typename _Alloc >	void get_as(const buffer< _Tp, _Alloc >&, _Vp&);	//Absolute get method	template < typename _Tp, typename _Vp, typename _Alloc >	void get_as(typename buffer< _Tp, _Alloc >::const_iterator, _Vp&);	//Relative put method	template < typename _Tp, typename _Vp, typename _Alloc >	void put_as(buffer< _Tp, _Alloc >&, const _Vp&);	//Absolute put method	template < typename _Tp, typename _Vp, typename _Alloc >	void put_as(typename buffer< _Tp, _Alloc >::iterator);	template < typename _Tp, typename _Vp, typename _Alloc >	buffer< _Vp, _Alloc > buffer_as(const buffer< _Tp, _Alloc >&);    //implements RAII, This makes exception safety easier, clients do not use this	template< typename Tp, typename Alloc >	struct BaseBuffer {		//buffer's implementation        struct _Buffer_impl : public Alloc {			// Invariants: mark <= position <= limit <= capacity	    Tp*			_data;            Tp*			_capacity;            Tp*			_limit;            Tp*			_position;            Tp*			_mark;			_Buffer_impl(const Alloc& __a)           : Alloc(__a), _data(0), _capacity(0), _limit(0), _position(0), _mark(0) { }		};          public:      typedef Alloc allocator_type;      allocator_type      get_allocator() const { return *static_cast<const Alloc*>(&this->_M_impl); }      BaseBuffer(const allocator_type& __a) : _M_impl(__a)      { }      BaseBuffer(size_t __n, const allocator_type& __a)		: _M_impl(__a) {			_M_impl._data		=	_M_allocate(__n);			_M_impl._position	=	_M_impl._data;			_M_impl._capacity	=	_M_impl._data + __n;		};      ~BaseBuffer() {		  _M_deallocate(_M_impl._data, _M_impl._capacity - _M_impl._data);	  }    public:      _Buffer_impl _M_impl;      Tp*      _M_allocate(size_t __n) { return _M_impl.allocate(__n); }      void      _M_deallocate(Tp* __p, size_t __n)	  { if (__p) _M_impl.deallocate(__p, __n); }    };	template < typename _Tp, typename _Alloc = std::allocator<_Tp> >	class buffer : protected BaseBuffer< _Tp, _Alloc > {				typedef BaseBuffer<_Tp, _Alloc>									_Base;		using _Base::_M_allocate;		using _Base::_M_deallocate;		using _Base::_M_impl;    public:      typedef _Tp														value_type;      typedef typename _Alloc::pointer									pointer;      typedef typename _Alloc::const_pointer							const_pointer;      typedef typename _Alloc::reference								reference;      typedef typename _Alloc::const_reference							const_reference;	  typedef pointer													iterator;	  typedef const_pointer												const_iterator;      typedef std::reverse_iterator<const_iterator>						const_reverse_iterator;      typedef std::reverse_iterator<iterator>							reverse_iterator;      typedef size_t													size_type;      typedef ptrdiff_t													difference_type;      typedef typename _Base::allocator_type							allocator_type;    public:      explicit      buffer(const allocator_type& __a = allocator_type())      : _Base(__a) { }	  explicit      buffer(size_type __n, const allocator_type& __a = allocator_type())      : _Base(__n, __a) {}	  template < typename U >	  buffer(const buffer<U>& __x);      buffer(const buffer& __x)      : _Base(__x.size(), __x.get_allocator())      { std::uninitialized_copy(__x._M_impl._data, (__x._M_impl._position - 1), this->_M_impl._data); }      template<typename _InputIterator>      buffer(_InputIterator __first, _InputIterator __last,			const allocator_type& __a = allocator_type());	  buffer& operator=(const buffer& __x);	  ~buffer() { clear(); }	  void clear() {		  pointer itr = _M_impl._data;		  while(itr != _M_impl._position)			  _M_impl.destroy(itr++);		  _M_deallocate(_M_impl._data, _M_impl._capacity - _M_impl._data);	  }	  size_type capacity() const;	  buffer& flip();	  buffer& flush() {		  pointer itr = _M_impl._data;		  while(itr != _M_impl._position)			  _M_impl.destroy(itr++);	  }      bool has_remaining() const;      bool is_read_only() const;      const_iterator limit() const;      buffer& limit(iterator);      buffer& mark();      const_iterator position() const;      buffer& position(iterator);      int remaining() const;      buffer& reset();      buffer& rewind();	  buffer duplicate();	  buffer slice() const;	  const_pointer array() const;	  const_reference operator[](size_type) const;	  reference operator[](size_type);// Transferring data operations	  //relative put	  buffer& put(const _Tp&);	  //absolute put	  buffer& put(iterator pos, const _Tp&);	  //relative bulk put	  template < typename InputIterator >	  buffer& put(InputIterator beg, InputIterator end);	  //absolute bulk put	  template < typename InputIterator >	  buffer& put(InputIterator beg, InputIterator end, iterator pos);	  //relative bulk put	  template < typename U >	  buffer& put(const buffer<U>&);	  //relative bulk put	  buffer& put(const buffer&);	  	  //relative get	  const_reference get() const;	  //relative get	  reference get();	  //Relative bulk get method.	  template < typename InputIterator >      buffer& get(InputIterator beg, InputIterator end) const;	  //absolute bulk get method	  template < typename InputIterator >      buffer& get(InputIterator beg, InputIterator end, const_iterator pos) const;	  const_iterator begin() const;	  iterator begin();	  const_iterator end() const;	  iterator end();	  const_reverse_iterator rbegin() const;	  reverse_iterator rbegin();	  const_reverse_iterator rend() const;	  reverse_iterator rend();	  size_type size() const { return size_type(end() - begin()); }      size_type max_size() const { return size_type(-1) / sizeof(value_type); }	  bool empty() const;	  	  void swap(buffer&);	};};#endif


sorry about how the indentation turned on this, i just copied & pasted it in.

This topic is closed to new replies.

Advertisement