2

I have an abstract (templated) class that I want to have its own return type InferenceData.

template <typename StateType>
class Model {
public:
    struct InferenceData;
    virtual InferenceData inference () = 0;
};

Now below is an attempt to derive from it

template <typename StateType>
class MonteCarlo : public Model<StateType> {
public:
    
    // struct InferenceData {};
    
    typename MonteCarlo::InferenceData inference () {
        typename MonteCarlo::InferenceData x;
        return x;
    }
};

This works, but only because the definition of MonteCarlo::InferenceData is commented out. If it is not commented, I get invalid covariant return type error. I want each ModelDerivation<StateType>::InferenceData to be its own type and have its own implementation as a struct. How do I achieve this?

user207421
  • 305,947
  • 44
  • 307
  • 483
Ryan Keathley
  • 49
  • 1
  • 5
  • How do you plan to use the `InterfaceData` returned by `interface`? Currently, even if it worked, anyone who called `interface` would have no clue what the thing it returned looks like. – Miles Budnek Jun 19 '22 at 05:21
  • 1
    You need to change Model::inference() to return a pointer (or reference) to `InferenceData`. Then you need to derive each sepcific `InferenceData` from `Model::InferenceData` and return a pointer (or ref) to it from each specific derived `inference()` method. – wohlstad Jun 19 '22 at 05:23
  • Thanks @wohlstad I forgot to derive the InferenceData. Is it really the case that this co-variance like this is only possible as pointers/refs at the return type? I tried using InferenceData in the arg and its not matching with the virtual function so I get abstract class initialization error – Ryan Keathley Jun 19 '22 at 05:37
  • @RyanKeathley added an answer with a complete example. – wohlstad Jun 19 '22 at 05:38

3 Answers3

2

You cannot change the return type of a derived virtual method. This is why your compilation failed when you try to return your derived InferenceData from MonteCarlo::inference().

In order to achieve what you need, you need to use a polymorphic return type, which requires pointer/reference semantics. For this your derived InferenceData will have to inherit the base InferenceData, and inference() should return a pointer/reference to the base InferenceData.

One way to do it is with a smart pointer - e.g. a std::unique_ptr - see the code below:

#include <memory>

template <typename StateType>
class Model {
public:
    struct InferenceData {};
    virtual std::unique_ptr<InferenceData> inference() = 0;
};


template <typename StateType>
class MonteCarlo : public Model<StateType> {
public:
    struct InferenceDataSpecific : public Model<StateType>::InferenceData {};

    virtual std::unique_ptr<Model::InferenceData> inference() {
        return std::make_unique<InferenceDataSpecific>();
    }
};

int main()
{
    MonteCarlo<int> m;
    auto d = m.inference();
    return 0;
}

Note: if you need to share the data, you can use a std::shared_ptr.

wohlstad
  • 12,661
  • 10
  • 26
  • 39
  • this is so close but due to performance concerns I don't want to use new. I want to pass a reference to an InferenceData object that is on the stack and change in place, or at the very least pass it by value since it is usually pretty light. – Ryan Keathley Jun 19 '22 at 05:45
  • Nvm https://stackoverflow.com/questions/11821158/c-covariance-in-parameters has the missing piece. Now I can make the return void and pass a ptr to something on the stack – Ryan Keathley Jun 19 '22 at 05:49
  • You can add a member to `MonteCarlo`: `InferenceDataSpecific m_curData;`, and instead of returning a smart pointer from `inference()` return a `InferenceData&` with a ref to the member. – wohlstad Jun 19 '22 at 05:49
  • @RyanKeathley I'm not sure I understood you, but returning a pointer to something on the stack is a bad idea (dangling pointer). Better return a pointer/reference to a data member. – wohlstad Jun 19 '22 at 05:52
  • The member and reference return type are good ideas. Thanks – Ryan Keathley Jun 19 '22 at 06:31
1

You have to make the return type part of the template arguments:

template <typename StateType, typename InferenceData>
class Model {
public:
    virtual InferenceData inference () = 0;
};

Then you can set the return type when you derive from it.

Goswin von Brederlow
  • 11,875
  • 2
  • 24
  • 42
  • 2
    I don't really accept this since I feel I shouldn't have to specify inference type since its tied to the DerivedModel type. – Ryan Keathley Jun 19 '22 at 05:42
  • If you want them tied together then you can always return a `StateType::InferenceData`. That's hiding the second template argument as part of the first tieing them together. Not uncommon. But either way is the only way to return the data by value. If you return by pointer or reference then you can return a derived classs of `InferenceData` in the derived class. – Goswin von Brederlow Jun 19 '22 at 13:57
1

You can actually have your MonteCarlo::inference return a pointer (or reference) to a MonteCarlo::InferenceData, as long as you do things correctly otherwise. A simple version looks like this:

#include <memory>
#include <iostream>

template <typename StateType>
class Model {
public:
    // base return type:
    struct InferenceData { };

    virtual InferenceData *inference() = 0;
};

template <typename StateType>
class MonteCarlo : public Model<StateType> {
public:
    // derived return type must be derived from base return type:
    struct InferenceData : public ::Model<StateType>::InferenceData { };

    InferenceData *inference() { return new InferenceData; }
};

int main() {
    MonteCarlo<int> mci;
    auto v = mci.inference();
}

This a a covariant return type (as the compiler alluded to in its error message). There are a couple of points to keep in mind here though:

  1. The return type really does have to be covariant. That is, the base class function has to be declared to return a pointer/reference to some base class, and the derived function has to return a pointer/reference to a type derived from that that the base function returns.
  2. A unique_ptr<Derived> allows implicit conversion to unique_ptr<Base>, assuming Derived is derived from Base, but a unique_ptr<Derived> still isn't actually derived from unique_ptr<Base>, so you can't use (most typical) smart pointers for covariant returns.

For the moment, I've used new to create the returned object. That's pretty common when dealing with derivation and such, but it can be avoided if necessary. Doing that can get non-trivial in itself, depending on your needs. In a really simple case, define a static object of the correct type, and return a pointer to it (but that leads to problems if you do multi-threading).

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • Is there any advantage of using such covariant return over the way I suggested in my answer (given that it doesn't support smart pointers well) ? – wohlstad Jun 19 '22 at 07:30
  • @wohlstad: The primary advantage would be if your derived class defines something new that a client can use directly via pointer to derived, that they'd have to use a downcast (e.g., `dynamic_cast`) to access if they receive a pointer to base. – Jerry Coffin Jun 19 '22 at 14:23
  • Good point. Although if you use a polymorphic method like `inference()` you usually access it via a base class pointer/reference. I also assumed that although the inference data might be different for each derived Model, it will have a common interface (inherited from `Model::InferenceData`). In this case no downcast will be needed. – wohlstad Jun 19 '22 at 14:33
  • @wohlstad: Yeah--it gets into all the usual "stuff" with designing an inheritance hierarchy. In an ideal world, you'd take everything needed into account when designing the base class, but reality often differs. – Jerry Coffin Jun 19 '22 at 14:39