Collection of objects of more than one type

蓝咒 提交于 2019-12-11 03:09:48

问题


Is there any non-awful way to have a collection of objects of more than one type? I'm perfectly happy to derive each type from a common base. I need sensible semantics so the collection can be copied, assigned, and so on.

Obviously, I can't just use a vector or list of the base class. Objects will be sliced and copying won't work at all. Using vectors or lists of pointers or smart pointers works, but you don't get sane copy semantics.

To get sane copy semantics, you need to use something like Boost's ptr_vector. But this requires a painful and error-prone infrastructure. Essentially, you can't just derive a new class from the base class because if it ever goes into the collection, it will not be properly copied.

This seems like such a common thing to do and all the solutions I know of are so awful. It seems like C++ is fundamentally missing a way to create a new instance of an object identical to a given instance -- even if that type has a copy constructor. And making a clone or duplicate function requires careful overloading in every derived class. If you fail to do it when creating a new class derived from the base (or any other class derived from that base) -- boom, your collection breaks.

Is there really no better way?


回答1:


You can use std::vector<boost::any> to do most of this I think.

#include "boost/any.hpp"
#include <vector>
#include <iostream>

//Simple class so we can see what's going on
class MM {
  public:
    MM()               { std::cout<<"Create @ "<<this<<std::endl; }
    MM( const MM & o ) { std::cout<<"Copy "<<&o << " -> "<<this<<std::endl; }
    ~MM()              { std::cout<<"Destroy @ "<<this<<std::endl; }
};

int main()
{
  //Fill a vector with some stuff
  std::vector<boost::any> v;
  v.push_back(0);
  v.push_back(0);
  v.push_back(0);
  v.push_back(0);

  //Overwrite one entry with one of our objects.
  v[0] = MM();

  std::cout<<"Copying the vector"<<std::endl;
  std::vector<boost::any> w;
  w = v;

  std::cout<<"Done"<<std::endl;
}

For which I get the output:

Create @ 0xbffff6ae
Copy 0xbffff6ae -> 0x100154
Destroy @ 0xbffff6ae
Copying the vector
Copy 0x100154 -> 0x100194
Done
Destroy @ 0x100194
Destroy @ 0x100154

Which is what I expect to see.

EDIT:

In line with your requirements to be able to treat members as some common base-type you'll need something very similar to boost::any, which thankfully is a relatively simple class.

template<typename BASE>
class any_with_base
{
    // ... Members as for boost::any

    class placeholder
    {
        virtual BASE * as_base() = 0;

        //Other members as in boost::any::placeholder
    };

    template<typename ValueType>
    class holder : public placeholder
    {
        virtual BASE * as_base() { return (BASE*)&held; }
        //Other members as in boost::any::holder<T>
    };

    BASE* as_base() { return content?content->as_base():0; }
}

Now you should be able to do this:

vector< any_with_base<Base> > v;
v.push_back( DerivedA() );
v.push_back( DerivedB() );

v[0].as_base()->base_fn();
v[1].as_base()->base_fn();

any_cast<DerivedA>(v[0])->only_in_a();

I actually dislike the syntax of any_cast and would use this opportunity to add an "as" member function.. so that I could write the last line as:

v[0].as<DerivedA>()->only_in_a();



回答2:


Alright, to follow up my comment, there is a way to do this without using boost::any that should offer superior performance in most cases, but it's admittedly a bit more involved. My solution combines two ideas: the use of a holder class that elides the type of its contents, and lightweight custom RTTI. We give the holder class meaningful copy and assignment semantics and we use containers of holders to manage the collection of objects. We use our lightweight RTTI to discover the true types of the objects when necessary. Here's some code to demonstrate what I'm proposing:

#include <vector>
#include <cassert>
#include <iostream>

#include <boost/cast.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/static_assert.hpp>

/// This template makes it possible to enforce the invariant that every type in a
/// hierarchy defines the id( ) function, which is necessary for our RTTI.  Using
/// a static assertion, we can force a compile error if a type doesn't provide id( ).

template< typename T >
struct provides_id {
  typedef char one;
  typedef long two;

  template< typename U, std::string const &(U::*)( ) const = &U::id >
  struct id_detector { };

  template< typename U > static one test( id_detector< U > * );
  template< typename U > static two test( ... );

  enum { value = sizeof(test<T>(0)) == sizeof(one) };
};

/// Base class for the holder.  It elides the true type of the object that it holds,
/// providing access only through the base class interface.  Since there is only one
/// derived type, there is no risk of forgetting to define the clone() function.

template< typename T >
struct holder_impl_base {
  virtual ~holder_impl_base( ) { }

  virtual T       *as_base( )       = 0;
  virtual T const *as_base( ) const = 0;

  virtual holder_impl_base *clone( ) const = 0;
};

/// The one and only implementation of the holder_impl_base interface.  It stores
/// a derived type instance and provides access to it through the base class interface.
/// Note the use of static assert to force the derived type to define the id( )
/// function that we use to recover the instance's true type.

template< typename T, typename U >
struct holder_impl : public holder_impl_base< T > {
  BOOST_STATIC_ASSERT(( provides_id< U >::value ));

  holder_impl( U const &p_data )
    : m_data( p_data )
  { }

  virtual holder_impl *clone( ) const {
    return new holder_impl( *this );
  }

  virtual T *as_base( ) {
    return &m_data;
  }

  virtual T const *as_base( ) const {
    return &m_data;
  }

private:

  U m_data;
};

/// The holder that we actually use in our code.  It can be constructed from an instance
/// of any type that derives from T and it uses a holder_impl to elide the type of the
/// instance.  It provides meaningful copy and assignment semantics that we are looking
/// for.

template< typename T >
struct holder {

  template< typename U >
  holder( U const &p_data )
    : m_impl( new holder_impl< T, U >( p_data ))
  { }

  holder( holder const &p_other )
    : m_impl( p_other.m_impl -> clone( ))
  { }

  template< typename U >
  holder &operator = ( U const &p_data ) {
    m_impl.reset( new holder_impl< T, U >( p_data ));
    return *this;
  }

  holder &operator = ( holder const &p_other ) {
    if( this != &p_other ) {
      m_impl.reset( p_other.m_impl -> clone( ));
    }

    return *this;
  }

  T *as_base( ) {
    return m_impl -> as_base( );
  }

  T const *as_base( ) const {
    return m_impl -> as_base( );
  }

  /// The next two functions are what we use to cast elements to their "true" types.
  /// They use our custom RTTI (which is guaranteed to be defined due to our static
  /// assertion) to check if the "true" type of the object in a holder is the same as
  /// as the template argument.  If so, they return a pointer to the object; otherwise
  /// they return NULL.

  template< typename U >
  U *as( ) {
    T *base = as_base( );

    if( base -> id( ) == U::static_id( )) {
      return boost::polymorphic_downcast< U * >( base );
    }

    return 0;
  }

  template< typename U >
  U const *as( ) const {
    T *base = as_base( );

    if( base -> id( ) == U::static_id( )) {
      return boost::polymorphic_downcast< U const * >( base );
    }

    return 0;
  }

private:

  boost::scoped_ptr< holder_impl_base< T > > m_impl;
};

/// A base type and a couple derived types to demonstrate the technique.

struct base {
  virtual ~base( )
  { }

  virtual std::string const &id( ) const = 0;
};

struct derived1 : public base { 
  std::string const &id( ) const {
    return c_id;
  }

  static std::string const &static_id( ) {
    return c_id;
  }

private:

  static std::string const c_id;
};

std::string const derived1::c_id( "derived1" );

struct derived2 : public base {
  std::string const &id( ) const {
    return c_id;
  }

  static std::string const &static_id( ) {
    return c_id;
  }

private:

  static std::string const c_id;
};

std::string const derived2::c_id( "derived2" );

/// A program to demonstrate that the technique works as advertised.

int main( ) {
  std::vector< holder< base > > vector1;

  vector1.push_back( derived1( ));
  vector1.push_back( derived2( ));

  std::vector< holder< base > > vector2 = vector1;

  /// We see that we have true copies!

  assert( vector1[0].as_base( ) != vector2[0].as_base( ));
  assert( vector1[1].as_base( ) != vector2[0].as_base( ));

  /// Easy assignment of container elements to new instances!

  vector2[0] = derived2( );
  vector2[1] = derived1( );

  // Recovery of the "true" types!

  std::vector< holder< base > >::iterator l_itr = vector1.begin( );
  std::vector< holder< base > >::iterator l_end = vector1.end  ( );

  for( ; l_itr != l_end; ++l_itr ) {
    if( derived1 *ptr = l_itr -> as< derived1 >( )) {
      std::cout << ptr -> static_id( ) << std::endl;
    }
    else if( derived2 *ptr = l_itr -> as< derived2 >( )) {
      std::cout << ptr -> static_id( ) << std::endl;
    }
  }
}

And here's the output:

derived1
derived2


来源:https://stackoverflow.com/questions/10663106/collection-of-objects-of-more-than-one-type

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!