开发者

Better way to write an object generator for an RAII template class?

开发者 https://www.devze.com 2022-12-28 11:10 出处:网络
I would like to write an object generator for a templated RAII class -- basically a function template to construct an object using type deduction of parameters so the types don\'t have to be specified

I would like to write an object generator for a templated RAII class -- basically a function template to construct an object using type deduction of parameters so the types don't have to be specified explicitly.

The problem I foresee is that the helper function that takes care of type deduction for me is going to return the object by value, which will (**) result in a premature call to the RAII destructor when the copy is made. Perhaps C++0x move semantics could help but that's not an option for me.

Anyone seen this problem before and have a good solution?

This is what I have:

template<typename T, typename U, typename V>
class FooAdder
{
private:
  typedef OtherThing<T, U, V> Thing;
  Thing &thing_;
  int a_;
  // many other members
public:
  FooAdder(Thing &thing, int a);
  ~FooAdder();
  FooAdder &foo(T t, U u);
  FooAdder &bar(V v);
};

The gist is that OtherThing has a horrible interface, and FooAdder is supposed to make it easier to use. The intended use is roughly like this:

FooAdder(myThing, 2)
  .foo(3, 4)
  .foo(5, 6)
  .bar(7)
  .foo(8, 9);

The FooAdder constructor initia开发者_开发百科lizes some internal data structures. The foo and bar methods populate those data structures. The ~FooAdder dtor wraps things up and calls a method on thing_, taking care of all the nastiness.

That would work fine if FooAdder wasn't a template. But since it is, I would need to put the types in, more like this:

FooAdder<Abc, Def, Ghi>(myThing, 2) ...

That's annoying, because the types can be inferred based on myThing. So I would prefer to create a templated object generator, similar to std::make_pair, that will do the type deduction for me. Something like this:

template<typename T, typename U, typename V>
FooAdder<T, U, V>
AddFoo(OtherThing<T, U, V> &thing, int a)
{
  return FooAdder<T, U, V>(thing, a);
}

That seems problematic: because it returns by value, the stack temporary object will (**) be destructed, which will cause the RAII dtor to run prematurely.

** - if RVO is not implemented. Most compilers do, but it is not required, and can be turned off in gcc using -fno-elide-constructors.


It seems pretty easy. The questioner himself proposed a nice solution, but he can just use a usual copy constructor with a const-reference parameter. Here is what i proposed in comments:

template<typename T, typename U, typename V>
class FooAdder
{
private:
  mutable bool dismiss;
  typedef OtherThing<T, U, V> Thing;
  Thing &thing_;
  int a_;
  // many other members
public:
  FooAdder(Thing &thing, int a);
  FooAdder(FooAdder const&o);
  ~FooAdder();
  FooAdder &foo(T t, U u);
  FooAdder &bar(V v);
};

FooAdder::FooAdder(Thing &thing, int a)
  :thing_(thing), a_(a), dismiss(false)
{ }

FooAdder::FooAdder(FooAdder const& o)
  :dismiss(false), thing_(o.thing_), a_(o.a_) 
{ o.dismiss = true; }

FooAdder::~FooAdder() {
  if(!dismiss) { /* wrap up and call */ }
}

It Just Works.

template<typename T, typename U, typename V>
FooAdder<T, U, V>
AddFoo(OtherThing<T, U, V> &thing, int a)
{
  return FooAdder<T, U, V>(thing, a);
}

int main() {
  AddFoo(myThing, 2)
    .foo(3, 4)
    .foo(5, 6)
    .bar(7)
    .foo(8, 9);
}

No need for complex templates or smart pointers.


You'll need a working copy constructor, but optimizing out such copies is explicitly allowed in the standard and should be quite a common optimization for compilers to make.

I'd say there's probably very little need to worry about the move semantics here (it is possible that it won't work anyway - see the auto_ptr_ref hackery that it takes for std::auto_ptr).


If you want to guarantee that what you want to do will work without using move semantics you need to do what auto_ptr does, which is maintain ownership state and provide a conversion operator to a type that transfers ownership between auto_ptrs.

In your case:

  1. Add a mechanism to indicate ownership in FooAdder. In FooAdder's destructor, only call the cleanup function if it has ownership.
  2. Privatize the copy constructor that takes a const FooAdder &; this prevents the compiler from using the copy constructor on rvalues which would violate your single owner invariant.
  3. Create an auxilary type (say, FooAdderRef) that will be used to transfer ownership between FooAdders. It should contain enough information to transfer ownership.
  4. Add a conversion operator (operator FooAdderRef) to FooAdder that relinquishes ownership in FooAdder and returns a FooAdderRef.
  5. Add a constructor that takes a FooAdderRef and claims ownership from it.

This is identical to what auto_ptr does in case you'd want to look at a real implementation. It prevents arbitrary copying from violating your RAII constraints while allowing you to specify how to transfer ownership from factory functions.

This is also why C++0x has move semantics. Because it's a giant PITA otherwise.


A friend template? (tested with gcc only)

template <class T, class U, class V> struct OtherThing
{
    void init() { }
    void fini() { }
};

template <class T, class U, class V>
class Adder
{
private:

    typedef OtherThing<T, U, V> Thing;
    Thing& thing_;
    int a_;

    Adder( const Adder& );
    Adder& operator=( const Adder& );

    Adder( Thing& thing, int a ) : thing_( thing ), a_( a ) {}

public:

    ~Adder() { thing_.fini(); }
    Adder& foo( T, U ) { return *this; }
    Adder& bar( V ) { return *this; }

    template <class X, class Y, class Z> friend
        Adder<X,Y,Z> make_adder( OtherThing<X,Y,Z>&, int );
};

template <class T, class U, class V>
Adder<T,U,V> make_adder( OtherThing<T,U,V>& t, int a )
{
    t.init();
    return Adder<T,U,V>( t, a );
}

int main()
{
    OtherThing<int, float, char> ot;
    make_adder( ot, 10 ).foo( 1, 10.f ).bar( 'a'
        ).foo( 10, 1 ).foo( 1, 1 ).bar( '0' );
    return 0;
}


Since C++03 requires explicitly spelling out the type in every declaration, there's no way to accomplish that without dynamic typing, eg having the template inherit from an abstract base class.

You did get something clever with

AddFoo(myThing, 2) // OK: it's a factory function
  .foo(3, 4)
  .foo(5, 6)
  .bar(7)
  .foo(8, 9); // but object would still get destroyed here

but it will be too much of a pain to code everything in that chain of calls.

C++0x adds auto type deduction, so look into upgrading your compiler, or enabling it if you have it. (-std=c++0x on GCC.)

EDIT: If the above syntax is OK but you want to have several chains in a scope, you could define a swap with void* operation.

 // no way to have a type-safe container without template specification
 // so use a generic opaque pointer
void *unknown_kinda_foo_handle = NULL;
CreateEmptyFoo(myThing, 2) // OK: it's a factory function
  .foo(3, 4)
  .foo(5, 6)
  .bar(7)
  .foo(8, 9)
  .swap( unknown_kinda_foo_handle ) // keep object, forget its type
  ; // destroy empty object (a la move)

// do some stuff

CreateEmptyFoo(myThing, 2) // recover its type (important! unsafe!)
  .swap( unknown_kinda_foo_handle ) // recover its contents
  .bar( 9 ) // do something
  ; // now it's destroyed properly.

This is terribly unsafe, but appears to fit your requirements perfectly.

EDIT: swap with a default-constructed object is also the answer to emulating move in C++03. You need to add a default constructor, and perhaps a resource-free default state wherein the destructor does nothing.


Here's one solution, but I suspect there are better options.

Give FooAdder a copy ctor with something similar to std::auto_ptr's move semantics. To do this without dynamic memory allocation, the copy ctor can set a flag to indicate that the dtor shouldn't do the wrap-up. Like this:

FooAdder(FooAdder &rhs) // Note: rhs is not const
  : thing_(rhs.thing_)
  , a_(rhs.a_)
  , // etc... lots of other members, annoying.
  , dismiss_(false)
{
  rhs.dismiss_ = true;
}

~FooAdder()
{
  if (!dismiss_)
  {
    // do wrap-up here
  }
}

It's probably sufficient to disable the assignment operator by making it private -- shouldn't be any need to call it.


When I consider problems like this, I usually prefer to think of the interface I wish to have first:

OtherThing<T,U,V> scopedThing = FooAdder(myThing).foo(bla).bar(bla);

I would propose a very simple solution:

template <class T, class U, class V>
class OtherThing: boost::noncopyable
{
public:
  OtherThing(); // if you wish

  class Parameters // may be private if FooAdder is friend
  {
  public:
    template<class,class,class> friend class OtherThing;
    Parameters(int,int,int);
    Parameters(const Parameters& rhs);  // proper resource handling
    ~Parameters();                      // proper resource handling

  private:
    Parameters& operator=(const Parameters&); // disabled

    mutable bool dismiss; // Here is the hack
    int p1;
    int p2;
    int p3;
  }; // Parameters

  OtherThing(const Parameters& p);
};

And then:

template <class T, class U, class V>
OtherThing<T,U,V>::Parameters fooAdder(Thing<T,U,V> thing, bla_type, bla_type);

There is no need for conversion operators and the like with which you risk to alter the noncopyable semantics, simply create a temporary struct from which your final class is constructible that will be used to pass all the parameters and alter the semantics of this struct for proper RAII. This way the final class OtherThing does not have screwed semantics and the nastiness (dismiss boolean) is safely tucked in a temporary that should never be exposed anyway.

You still need to make sure for proper exception handling. Notably it means that the temporary struct is responsible for the resource as long as it's not passed to OtherThing.

I know it does not seem to bring much to the table since you're basically going to hack Parameters instead of OtherThing, but I urge you to consider what this would mean:

OtherThing<T,U,V> scopedThing = /**/;
OtherThing<T,U,V>* anotherThing = new OtherThing<T,U,V>(scopedThing);

The second line is valid with your tentative hacks, since scopedThing can be taken by reference as well as const reference, but it does screw things up as much as it does with std::auto_ptr. In the same vein, you can then have std::vector< OtherThing<T,U,V> > and the compiler is never going to complain...

0

精彩评论

暂无评论...
验证码 换一张
取 消