views:

126

answers:

6

Let's say I have the following class hierarchy in C++:

class Base;
class Derived1 : public Base;
class Derived2 : public Base;


class ParamType;
class DerivedParamType1 : public ParamType;
class DerivedParamType2 : public ParamType;

And I want a polymorphic function, func(ParamType), defined in Base to take a parameter of type DerivedParamType1 for Derived1 and a parameter of type DerivedParamType2 for Derived2.

How would this be done without pointers, if possible?

+2  A: 

You are looking for covariant parameters. This isn't possible in C++, which only supports covariant return types.

This isn't simply an omission in the language. What you are trying to do doesn't actually make sense. Imagine, for instance, you could define the following setup:

class Decoder { virtual void decode(Stream*); };
class Base64Decoder { void decode(Base64Stream*); };
class GZipDecoder { void decode(GZipStream*); };
...
Decoder* d = new Base64Decoder;
d.decode(new GZipStream("file.gz"));

Since Decoder::decode() accepts any Stream, the last line is valid code. If virtual function rules allowed what you want, Base64Decoder::decode would be passed a GZipStream.

Marcelo Cantos
For input-only parameters, contra-variant parameters satisfy the LSP, although C++ doesn't permit them. And you're absolutely right that covariant parameters aren't valid, even theoretically (they would have to be output-only, and C++ doesn't have that concept).
Ben Voigt
+2  A: 

You cannot have Base::func take different parameters depending on what class inherits it. You will need to change something.

You could make them both take a ParamType and handle an unexpected parameter with whatever mechanism you like (e.g. throw an exception or return an error code instead of void):

struct ParamType;
struct Base {
  void func(ParamType&);
}
struct Derived1 : Base {};
//...

Or template on the type of parameter they should take:

struct ParamType;
struct DerivedParamType1 : ParamType {};
struct DerivedParamType2 : ParamType {};

template<class ParamT>
struct Base {
  void func(ParamT&);
};
struct Derived1 : Base<DerivedParamType1> {};
struct Derived2 : Base<DerivedParamType2> {};

With the second solution, Derived1 and Derived2 won't share a common base and cannot be used polymorphically.

Roger Pate
Theoretically, overriding functions need a compatible signature, not identical. In fact, C++ does allow covariant return types. However, input parameters are contra-variant while non-const pointers and references are invariant.
Ben Voigt
@Ben: I'm not sure what you're trying to point out about my answer.
Roger Pate
+1  A: 

This is called double dispatch, although this will have to be done with pointers to the base class, that's how polymorphism works. In C++, double dispatch is not supported directly, so there is some work involved.

stefaanv
+2  A: 

That defeats the purpose of polymorphism; if Base provides a function Base::func(const ParamType&), then that same function (or an override of it) needs to accept const ParamType& in Derived1. You can provide an overload for func that is specialized on const DerivedParamType1&.

The closest thing to what you are looking for is to provide such a specialized overload, and then make Derived1::func(const ParamType&) private. Note, however, that this breaks polymorphism. The whole point of polymorphism is that if you can call the function on the base type, then you can call that same function (with the same parameters) on any class that inherits from it, which is clearly not the case.

Michael Aaron Safyan
A: 

As stefaanv says, this can be achieved with double dispatch with some extra plumbing:

#include <iostream>

class Derived1;
class Derived2;

class ParamType
{
public:
    virtual void invertFunc (const Derived1& deriv) const = 0;
    virtual void invertFunc (const Derived2& deriv) const = 0;
};

class DerivedParamType1;
class DerivedParamType2;

class Base
{
public:
    virtual void func (const ParamType& param) const = 0;

    virtual void func (const DerivedParamType1& param) const
    {
        throw std::runtime_error ("Can not accept DerivedParamType1");
    }

    virtual void func (const DerivedParamType2& param) const
    {
        throw std::runtime_error ("Can not accept DerivedParamType2");
    }
};

class Derived1 : public Base
{
public:
    void func (const ParamType& param) const
    {
        param.invertFunc (*this);
    }

    void func (const DerivedParamType1& param) const
    {
        std::cout << "Derived1::func (DerivedParamType1)" << std::endl;
    }
};

class Derived2 : public Base
{
public:
    void func (const ParamType& param) const
    {
        param.invertFunc (*this);
    }

    void func (const DerivedParamType2& param) const
    {
        std::cout << "Derived2::func (DerivedParamType2)" << std::endl;
    }
};

class DerivedParamType1 : public ParamType
{
public:
    void invertFunc (const Derived1& deriv) const
    {
        deriv.func (*this);
    }

    void invertFunc (const Derived2& deriv) const
    {
        deriv.func (*this);
    }
};

class DerivedParamType2 : public ParamType
{
public:
    void invertFunc (const Derived1& deriv) const
    {
        deriv.func (*this);
    }

    void invertFunc (const Derived2& deriv) const
    {
        deriv.func (*this);
    }
};


int main (int argc, char* argv[])
{
    ParamType* paramType = new DerivedParamType1;
    Base* deriv = new Derived1;

    deriv->func (*paramType);

    return 0;
}

Note that there's actually 3 jumps (dispatches) here as you asked for Base::func(ParamType) to call Derived1::func(DerivedParamType1). If you are happy with either:

Base::func(ParamType) calls DerivedParamType1::func(Derived1)

or

ParamType::func(Base) calls Derived1::func(DerivedParamType1)

then you can eliminate one of the jumps.

jon hanson
+1  A: 

If these (operator class and parameter type) are separate concepts then they should remain separate. reinterpret_cast or something in your overridden methods, but if they're orthogonal then it doesn't make sense to do what you're asking.

If they aren't actually separate, make that obvious and do away with the whole notion of virtual functions, since that's not what you want. You know the type of object you have and you know the type of parameters, so in this case there's nothing 'virtual' about anything.

class Base
{
public:
   class ParamType { }

   void DoSomething(const ParamType&); // called by derived classes as necessary
};

class Derived1 : public Base
{
public:
   class DerivedParamType1 : public ParamType { }

   void DoSomething(const DerivedParamType1&);
};

class Derived2 : public Base
{
public:
   class DerivedParamType2 : public ParamType { }

   void DoSomething(const DerivedParamType2&);
};
dash-tom-bang