Topic: auto_res template


Author: "Omry Yadan" <omry_y@inter.net.il>
Date: 1999/03/24
Raw View
Hi all.

There is a potential problem when using exceptions :
concider :

void t()
{
   /*...*/
   if(in_a_mess()) throw mess();
}

void f()
{
   int *a = new int(1);
   FILE *f=fopen("myfile.dat","rw");
   t();
   delete a;
   fclose(f);
}

its quite easy to see what happens if t() gets into mess.
a mess will be thrown, causing the resources that was allocates at the
begining of f() not to be freed.
a simple workaround is to change f() :

void f1()
{
   int *a = new int(1);
   FILE *f=fopen("myfile.dat","rw");
   try
   {
      t();
   }
   catch(mess)
   {
     delete a;
     fclose(f);
   };
   delete a;
   fclose(f);
}

but this workaround presents few problems :
1. the function size was doubled.
2. its tedious, and humans tend to skip tedious things.
   eventualy, a programer will not warp a call for t() with such a try
block, and we will have a potenial leak of
   resources.

fortunately, we have a tool that can do the dirty work for us : scope.
every object is destroyed when it gets out of scope.
so we can warp the FILE* in a class like :

class FILE_ptr
{
   FILE *_f;
public:
   FILE_ptr(FILE *f) : _f(f) {}
   FILE_ptr(char *n,char *a) {_f = fopen(n,a);}
   ~FILE_ptr() {fclose(_f);}
   operator FILE*() {return _f;}
};

and now our f() can be changed to :

void f2()
{
   FILE_ptr f("myfile.dat","rw");
   t();
}

the absolute minimum.
if t() throws mess, f will be destroyed properly, closing the FILE.

this technique is presented in the Stroustrup`s TC++PL 3rd ed, as "resource
acquisition is initialization".
this means the initializing the object is acquiring the resource, and that
destroying the object is freeing the resource.

unfortunately, there are several holes in such a simple approach :
concider :

void f3()
{
   FILE_ptr f1("myfile.dat","rw");
   FILE_ptr f2=f1;
}

guess what will happened?
correct : the file will be closed twice.
once when f1 gets out of scope, and one when f2 does.

the template class auto_ptr<t> is an exception safe pointer holder.
the solution presented by the auto_ptr class is called destructive copy.
when one auto_ptr is copied (copy ctor or operator=) to another, the
original changes, and is no longer the owner of the memory, therefor
its no longer  his job to free the allocated memory.

auto_ptr<int> i1 = new int;         // now i1 should free the int.
auto_ptr<int> i2=i1;                    // now i2 should free the int., and
i1 not.

due to the fact that every type can be allocated using new, and deallocaetd
using delete,
auto_ptr is quite easy to impement.
but what if we want to create an auto_res template, that will hold any kind
of resource, and will know how to allocate and how to free it?
since resources differ in the allocation and deallocation methods, the
template should include a type for
initialzing the resource, and a type for freeing it.

template <class T, class INIT, class FREE>
class auto_res {/*....*/};

typical allocator have the form :
T init();

and a typical deallocator has the form :
void free(T);

lets assume our resource is a pointer to
struct data{/*....*/};

allocator to data* :
data* a_data() {return new data;}

deallocator to data* :
void d_data(data* d) {delete d;}

lets typedef a auto_res to data* :
typedef auto_res<data*,data*(*)(), void(*)(data*)> auto_data;
                                  1           2                  3
1 - the resource type.
2 - type of allocator function.
3 - type of deallocator function.

now lets use it  :

void f4()
{
    auto_data ad(&d_alloc,&d_free); // use of & to make use of pointer to
function obvious.
    t();
}

we just used our first auto_res.
if t() throws mess, ad will get properly destroyed.

now, life are not always that simple - as most of the time, when we
construct an object, we need to pass it
initializing arguments.
since a typical allocator is of the form
T init();
we cant just pass arguments to it.
a good solution will be to use a  function object.
a obvious example of a type that need arguments to be initialized is a
FILE*.
let us use only function object this time :

struct f_open
{
   char *name;
   char *attr;
   f_open(char *n,char *a) : name(n), attr(a) {}
   FILE* operator()() {return fopen(name,attr);}
};

struct f_close
{
    void operator()(FILE*f) {fclose(f);}
};

typedef auto_res<FILE*,f_open,f_close> auto_fp

now we can use auto_pf  to hold FILE* like this :

void f5()
{
    auto_fp( f_open("myfile.dat","rw"), fclose() );
}

now, why do we need two seperate objects?
since a typical allocator and a typical deallocator do not have the same
signature, we can put them
both in the same class!

struct chars_handler
{
  size_t _sz;
  chars_handler(size_t sz = 0) : _sz(sz) {}
  char *operator()() { return new char[_sz]; }  // Allocates memory.
  void operator()(char* p) { delete[]p; }           // Free memory.
};

void f6()
{
   auto_res<char*, chars_handler, chars_handler>
         auto_chars(char_handler(100),chars_handler());
}

since we are using functions, we can do whatever we want, for example,
suppose I have a function that
set the screen to graphic mode at the begining, and restore it to text mode
at the end,
I can make it exception safe like this :

struct set_vmode
{
  int _mode;
  set_vmode(int mode) : _mode(mode) {}
  int operator()()    // will be used to set.
    {cout << "setting mode " << _mode << endl;return _mode;}
  void operator()(int dummy) // will be used to set back.
    {cout << "setting mode " << _mode << endl;}
};

to avoid getting into useless details about screen modes, I just used
output.

void ar5()
{
    auto_res<int,set_vmode,set_vmode>
        auto_vmode(set_vmode(1),set_vmode(0));
}

have a look at the function
void operator()(int dummy)
defined in set_vmode.
why dummy?
bcoz it does nothing with it, it uses the _mode variable, that was set uppon
construction of the set_vmode
object.
yet, the deallocator function must follow the typical signature of
void free(T);

my compiler does not support template member functions, if it did - I could
have make the use of the auto_res as simple as :

auto_res<FILE*> fp(file_open("name","r"), file_close() );
(let the compiler deduce the type of INIT and FREE instead of having to
explicity define them.

my latest header file that implement the auto_res is included, please try it
out, and notify me if u encounter bugs or have any suggestions.

comments are more then welcomed.

--
   Omry Yadan.




      [ Send an empty e-mail to c++-help@netlab.cs.rpi.edu for info ]
      [ about comp.lang.c++.moderated. First time posters: do this! ]

[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: sbnaran@bardeen.ceg.uiuc.edu (Siemel Naran)
Date: 1999/03/26
Raw View
>void f1()
>{
>   int *a = new int(1);
>   FILE *f=fopen("myfile.dat","rw");
>   try
>   {
>      t();
>   }
>   catch(mess)
>   {
>     delete a;
>     fclose(f);
>   };
>   delete a;
>   fclose(f);
>}

Two errors.
1. If fopen(...) throws, then 'a' not deleted.
   Of course, std::fopen(...) doesn't throw, but this is beside
   the point.  Solution: wrap 'a' in an auto_ptr.
2. If t(...) throws a mess, then 'a' will be deleted twice.
   The first delete is fine; the second is likely a program crash.
   Similar problem for 'f'.  Solution: set 'a' and 'f' to null in
   the catch block.  Better solution: wrap 'a' in an auto_ptr and
   'f' in a file_ptr (note: class std::fstream is like a file_ptr)
   and let the destructor do the appropriate de-allocation.


>but this workaround presents few problems :
>1. the function size was doubled.
>2. its tedious, and humans tend to skip tedious things.
>   eventualy, a programer will not warp a call for t() with such a try
>   block, and we will have a potenial leak of resources.

1. Do you mean executable code size or source code size?  I don't
   think there's anything you can do about executable code size.
   But source code size and readability can be improved if we use
   auto_ptr and file_ptr (or std::fstream).

2. Good point.



>class FILE_ptr
>{
>   FILE *_f;
>public:
>   FILE_ptr(FILE *f) : _f(f) {}
>   FILE_ptr(char *n,char *a) {_f = fopen(n,a);}
>   ~FILE_ptr() {fclose(_f);}
>   operator FILE*() {return _f;}
>};

You should probably disallow the copy constructor and operator= by
declaring them private and not giving them an implementation.  When
designing any class, think carefully if you need a copy constructor
and operator=.  In this case, what does it mean to copy a FILE_ptr?
One could come up with a reasonable meaning, but most people don't
have a reasonable meaning.  Do you re-open the file or not?  If
re-open, then where do you put the get/set pointer?  Note that
class std::fstream does not have a copy constructor or operator=.

Also, when our destructor has side effects, we should disallow the
copy constructor.  Consider this function,
   FILE_ptr f() { return FILE_ptr("hello","world"); }
The return value optimization stipulates that in a call to
f(), there are 0 calls to the copy constructor and 0 calls to the
destructor.  But a compiler doesn't have to do the return value
optimization.  It can create the FILE_ptr object as a local
temporary variable and then put this temporary into the return
value space, thus using 1 call to the copy constructor and 1 call
to the destructor.  So if X::X(const X&) or X::~X() have side
effects and we return X objects, then our program has unpredictable
results: the side effects might occur 0 times or 1 times.
(At present, most compilers do the return value optimization
for unnamed locals.)

The one-arg constructor should be explicit.  (Also an important
consideration.)



>guess what will happened?
>correct : the file will be closed twice.
>once when f1 gets out of scope, and one when f2 does.

To handle this case, one should have a reference count.  Only
when 'f2' goes out of scope will the file be closed.  Or take the
simple route: disallow the copy constructor and operator= once and
for all, as explained above.



>template <class T, class INIT, class FREE>
>class auto_res {/*....*/};

Fine, but how much writing is it to write hand tailored auto_ptr
and file_ptr classes?  Besides, these are already done for us.


>   auto_data ad(&d_alloc,&d_free);
>   auto_fp( f_open("myfile.dat","rw"), fclose() );

You must have meant "f_close()" for the second line.

To repeat, where is there more writing: in
   auto_ptr and file_ptr
   f_open f_close auto_res<T>
I think both have just as much writing, so I'd use the first
simpler one.

--
---------------
Siemel B. Naran
---------------
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]






Author: Zeisel Helmut VAI/TAP 1 <Zeisel@linz.vai.co.at>
Date: 1999/03/26
Raw View
> In article <slrn7finr8.r68.sbnaran@bardeen.ceg.uiuc.edu>,
> sbnaran@bardeen.ceg.uiuc.edu (Siemel Naran) writes:
> >In article <7d7u7v$31k$3@news2.inter.net.il>, "Omry Yadan"
> <omry_y@inter.net.il>>void f1()
> >>{
> >>   int *a = new int(1);
> >>   FILE *f=fopen("myfile.dat","rw");
> >>   try
> >>   {
> >>      t();
> >>   }
> >>   catch(mess)
> >>   {
> >>     delete a;
> >>     fclose(f);
> >>   };
> >>   delete a;
> >>   fclose(f);
> >>}
> >
> >Two errors.
> >1. If fopen(...) throws, then 'a' not deleted.
> >   Of course, std::fopen(...) doesn't throw, but this is beside
> >   the point.  Solution: wrap 'a' in an auto_ptr.
> >2. If t(...) throws a mess, then 'a' will be deleted twice.
> >   The first delete is fine; the second is likely a program crash.
> >   Similar problem for 'f'.  Solution: set 'a' and 'f' to null in
> >   the catch block.  Better solution: wrap 'a' in an auto_ptr and
> >   'f' in a file_ptr (note: class std::fstream is like a file_ptr)
> >   and let the destructor do the appropriate de-allocation.
> >
> >
>
Is file_ptr really defined in ISO C++?

> >
> >
> >>template <class T, class INIT, class FREE>
> >>class auto_res {/*....*/};
> >
> >Fine, but how much writing is it to write hand tailored auto_ptr
> >and file_ptr classes?  Besides, these are already done for us.
> >
> >
>
I use s similar approach in my reference counting
pointer class, say

template<class T, class FREE=DEFAULT_DELETER>
class refcnt_ptr
{
...
};

I have no need for INIT, but when necessary, it could easily be extended.
Observe that there is a default parameter, so there is no need
for hand tailoring when one uses DEFAULT_DELETER
(with the obvious implementation)

Clearly this could easily be part of a revised auto_ptr
without breaking existing code,
possible applications are
  calling ANSI C free() (when callling code from a C library),
  new[], delete[] pairs,
  f_close(),
  CORBA::release
  ...

In particular, I would like that future standard smart pointer
classes contained a similar second parameter.

Helmut Zeisel


---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]





Author: sbnaran@bardeen.ceg.uiuc.edu (Siemel Naran)
Date: 1999/03/26
Raw View
On 26 Mar 99 17:38:40 GMT, Zeisel Helmut  VAI/TAP 1

>Is file_ptr really defined in ISO C++?

Literally, no.  There is no such thing as std::file_ptr.

Metaphorically, yes.  Because std::fstream is like std::file_ptr.
That is, we can think of a std::fstream as a pointer to a file,
so when the fstream goes out of scope, the file is closed.

--
----------------------------------
Siemel B. Naran (sbnaran@uiuc.edu)
----------------------------------
---
[ comp.std.c++ is moderated.  To submit articles, try just posting with ]
[ your news-reader.  If that fails, use mailto:std-c++@ncar.ucar.edu    ]
[              --- Please see the FAQ before posting. ---               ]
[ FAQ: http://reality.sgi.com/austern_mti/std-c++/faq.html              ]