2

I came up with a class that uses a protected nested struct, intending for derived classes to augment the struct. To that end, I declared a virtual method for allocating the struct.

Now, the base class does some not-trivial amount of work in processSomeData and I would like the derived class to reuse it.

Which leads to the following:

class A
{
public:
    virtual void doProcessing(); // uses processSomeData()

protected:
    struct someData
    {
        virtual ~someData() {};

        // data members
    };

    virtual someData* processSomeData(); // uses allocateSomeData()
    virtual someData* allocateSomeData();
};

class B : public A
{
public:
    virtual void doProcessing()
    {
        derivedData* myData =
            static_cast<derivedData*>(A::processSomeData()); // *** seems a little suspect

        // do work on the additional data members in myData
    }

protected:
    struct derivedData : public someData
    {
        // more data members
    };

    virtual derivedData* allocateSomeData();
};

Because allocateSomeData is overridden, I know A::processSomeData is returning a someData* pointing to a derivedData, so static_cast is definitely safe.

That said, it feels a little off that I should have to cast from a base to derived at all, when everything else seems pretty kosher.
Is there a better/proper way to do this, without using a cast? Or do I have to re-design my classes/structs?

aerobot
  • 195
  • 1
  • 8
  • What seems wrong is inheriting `derivedData` from `someData`, do they share an interface? Can't you do what you want with aggregation? – imreal Jun 13 '15 at 19:31
  • @imreal - I used the 'data' in the genericised example, the struct are related and inheritance does seem logically sound, although I can imagine how aggregation might be used instead. – aerobot Jun 14 '15 at 01:43
  • If you `dynamic_cast` you will at least get a detectable error instead of UB in case of a mistake – M.M Jun 26 '15 at 06:14

3 Answers3

1

It is because the compiler doesn't know for sure that processSomeData uses allocateSomeData to create the someData struct. So far as the compiler knows the someData, that is returned from processSomeData might well be just an instance of someData. derivedData is someData but not the other way round.

Hauke S
  • 555
  • 5
  • 10
  • I considered adding a template parameter to A, but that would mean A receiving a template argument which is its own non-public nested type. I can't template at the method level because A has a member which is a someData pointer. – aerobot Jun 14 '15 at 02:12
  • Do you need to return the someData* in the processSomeData method or could it just operate on the member? – Hauke S Jun 14 '15 at 17:58
  • Well, the derived class B actually does a bit more than its base class A, and the intention was to take the aliased someData and do the additional bits of processing. – aerobot Jun 15 '15 at 00:33
  • Anyway, I did a bit more looking around and came to realised this is the classic downcasting problem. I hadn't realised it because it was happening inside a hierarchy and involved a nested type. – aerobot Jun 15 '15 at 00:35
1

While the template argument is a good method, let me put a vote in for another solution.

First we move processSomeData into the nested someData struct, keeping it virtual. Its implementation does all the work common to someData and its derived classes. We also have a new protected virtual function, call it furtherProcess. For someData it is empty. For every derived class, it handles whatever is needed. The last line of someData::processSomeData() is furtherProcess().

This use of a hook function at the end avoids the Call Super code smell implicit in the original set-up, which is often seen with these downcasts.

Andrew Lazarus
  • 18,205
  • 3
  • 35
  • 53
  • 1
    Thanks for the input, very much appreciated! The term Call Super is new to me, but very relevant. I understand the solution pattern you described, but unfortunately, implementing it will introduce its own difficulties, which are not apparent because the example snippet I provided is neither a full description of my class nor its context. Hence the comment about the XY problem in my answer. – aerobot Jun 26 '15 at 07:28
0

I resolved the issue by moving the nested class out and making it a template argument, because I would never use someData and derivedData concurrently.

struct someData
{
    virtual ~someData() {};

    // data members
};

template <typename DataType = someData>
class A
{
public:
    virtual void doProcessing(); // uses processSomeData()

protected:
    typedef DataType myDataType;

    virtual myDataType* processSomeData(); // uses allocateSomeData()
    virtual myDataType* allocateSomeData();
};

struct derivedData : public someData
{
    // more data members
};

class B : public A<derivedData>
{
public:
    virtual void doProcessing()
    {
        myDataType* myData = A::processSomeData();

        // do work on the additional data members in myData
    }

protected:

    virtual myDataType* allocateSomeData();
};

While the nested class looked like a good way to encapsulate information, it did not seem to be worth the trade-off between type-safety and performance.

This answer, discovered shortly after I made the change, seems to somewhat vindicate the decision.

Community
  • 1
  • 1
aerobot
  • 195
  • 1
  • 8
  • I would also note that my question suffered from the [XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – aerobot Jun 26 '15 at 06:07