How can I use std::generate/generate_n with a polymorphic function object?

2.2k views Asked by At

I'm new to std::generate and have attempted to structure a program which uses it to initialize vectors. However it's behaving differently to my expectations.

I have an abstract base class:

template <typename G>
class RandomAllele {
 public:
  RandomAllele() { /* empty */ }
  virtual ~RandomAllele() { /* empty */ }

  virtual G operator()() const = 0;
}; 

Which is extended by (for example):

class RandomInt : public RandomAllele<int> {
 public:
  RandomInt(int max) : max_(max) {}
  int operator()() const { return rand() % max_; }
 private:
  int max_;
};

I pass an instance of my inheriting class to a factory class by pointer, and then use it as the third argument for std::generate:

template<typename G, typename F>
class IndividualFactory {
 public:
  IndividualFactory(int length, const RandomAllele<G> *random_allele)
      : length_(length), random_allele_(random_allele) { /* empty */ }

  individual_type *generate_random() const {
    std::vector<G> *chromosome = new std::vector<G>(length_);
    std::generate(chromosome->begin(), chromosome->end(), *random_allele_); */

    return new individual_type(chromosome);
  }
 private:
  int length_;
  RandomAllele<G> *random_allele_;
};

Now I get an error saying that RandomAllele cannot be instantiated because it's an abstract class. Why does generate need to instantiate it when the pointer already exists? And why is it trying to use the base class instead of the inheriting class RandomInt?

This works fine if I replace std::generate with:

for(auto iter = chromosome->begin(); iter != chromosome->end(); ++iter)
  *iter = (*random_allele_)();

But I still wish to understand why it behaves strangely, and I'd prefer to use generate if there is a way to do this.

Thanks for your time,

Rhys

5

There are 5 answers

1
templatetypedef On BEST ANSWER

As others have mentioned above, the generate and generate_n functions take their generator objects by value, precluding you from directly using inheritance in this context.

However, one trick you can do is to apply the Fundamental Theorem of Software Engineering:

Any problem can be solved by adding another layer of indirection

Rather than directly passing in a polymorphic functor, instead pass in a wrapper functor that stores a pointer to this polymorphic functor and then forwards the call appropriately. For example:

template <typename T> class IndirectFunctor {
public:
    explicit IndirectFunctor(RandomAllele<T>* f) : functor(f) {
         // Handled in initializer list
    }

    T operator() () const {
        return (*functor)();
    }

private:
    RandomAllele<T>* functor;
};

If you then pass this object into generate, as seen here:

RandomAllele<T>* functor = /* ... create an allele ... */
std::generate(begin, end, IndirectFunctor<T>(functor));

Then everything will work as intended. The reason for this is that if you copy IndirectFunctor<T> by value, then you just shallow-copy the stored pointer, which will still point to the RandomAllele you want to call. This avoids the slicing problem you were encountering because it never tries directly copying an object of type RandomAllele through a base class pointer. It always copies the wrapper object, which never tries to duplicate RandomAllele.

Hope this helps!

2
Erik On

std::generate's generator is passed by value, and therefore copied.

2
Keith On

The prototype is:

template <class ForwardIterator, class Generator>
void generate ( ForwardIterator first, ForwardIterator last, Generator gen );

Hence gen is passed by value, so the compiler attempts to construct a RandomAllele by copy, hence problem.

The solution is to use an Envelope to provide the needed indirection:

template<class G>
class RandomAlleleEnvelope
{
public:
    RandomAlleleEnvelope(const RandomAllele<G>* ra)
        : ra_(ra)
    {}
      int operator()() const { return (*ra_)(); }
private:

    const RandomAllele<G>* ra_;
};

  std::generate<std::vector<int>::iterator,RandomAlleleEnvelope<int> >(chromosome->begin(), chromosome->end(), random_allele_); 

Also note there is another solution, define your own generate to use a reference:

template <class ForwardIterator, class Generator>
  void referenceGenerate ( ForwardIterator first, ForwardIterator last, 
                           const Generator& gen )
{
  while (first != last)  *first++ = gen();
}
 referenceGenerate(chromosome->begin(), chromosome->end(), *random_allele_); 

I also think the following should work, that is to use the standard generate and explicitly make it handle a reference type:

std::generate<std::vector<int>::iterator, const RandomAllele<int>& >
                   (chromosome->begin(), chromosome->end(), *random_allele_); 

I say should because this fails is instantiate on VS2010. On the other hand, if I can define my own:

  template <class ForwardIterator, class Generator>
  void myGenerate ( ForwardIterator first, ForwardIterator last, Generator gen )
  {
     while (first != last)  *first++ = gen();
  }
  myGenerate<std::vector<int>::iterator, const RandomAllele<int>& >
        (chromosome->begin(), chromosome->end(), *random_allele_); 

The VS2010 fails because it implements std::generate is terms of another std::generate which defaults back to non-reference parameters.

0
Mark B On

In general the C++ standard library implements static polymorphism (templates) and doesn't support runtime polymorphism (virtual methods) for function objects. This is because it passes all its function objects by values, assuming them to be stateless or almost stateless such that the added indirection of passing by pointer or reference would be more expensive than by value.

Since it's passed by value this results in slicing and when you try to use a RandomAllele<G> it thinks you mean that exact class not whatever derived type it actually points to. Instead of templating on G just template on the exact generator functor type you desired directly.

0
wilhelmtell On

The issue is that all standard algorithms take their arguments by value, to conform with traditional C constraints. So here the std::generate() algorithm take the functor by value. Your functor, of type RandomAllele<int>, is of abstract type. Yes, it's a pointer pointing at a concrete type, but the pointer is of an abstract type. In copying this object, the algorithm calls the copy constructor of RandomAllele<int>; i.e., the algorithm constructs an instance of abstract type. And this is something the C++ language forbids.

You can tell the runtime environment not to worry too much like so:

RandomInt *cp = dynamic_cast<RandomInt*>(random_allele);
if( ! cp ) {
    // i thought the pointer is of RandomInt. It isn't. Err.
    std::terminate(); // or something
}
std::generate(chromosome->begin(), chromosome->end(), *cp);