Getting an optional of a class derived from abstract class, from a class derived from abstract class

101 views Asked by At

I have something similar to the code below, where a group of classes with similar shared behaviours (Tool1, Tool2), all inherit from an abstract class (ITool). All these classes own their own optional of a corresponding class (Tool1Attachment, Tool2Attachment), which also inherit from an abstract class (IAttachment).

class ITool {
 private:
  virtual ? DoGetAttachment() = 0;
  // Other shared behaviours...

 public:
  ? GetAttachment() {return DoGetAttachment();}
  // Other shared behaviours...
};

class Tool1: public ITool {
  std::optional<Tool1Attachment> opt_attachment;

  ? DoGetAttachment() override;
  
 public:
  [...]
};

class Tool2: public ITool {
  std::optional<Tool2Attachment> opt_attachment;
  
  ? DoGetAttachment() override;
  
 public:
  [...]
};

// --------------------------------

class IAttachment {
  [...]
};

class Tool1Attachment : public IAttachment {
  [...]
};

class Tool2Attachment : public IAttachment {
  [...]
};

It makes sense for the tool class to have an optional - in context, it may or may not have an actual instance at any given time.

An issue arises if I have an IAttachment, and would like to get the IAttachment.

I originally used pointers for this, i.e. IAttachment* GetAttachment();, which worked "fine". However, it seems to lose some of the more explicit checking of an absent value that optional provides, making it easier to run into nullptr issues in the future.

I also tried different constructs (std::reference_wrapper) with optionals but kept running into invalid covariant errors.

Is there a way of doing this that allows the return of an optional (or similar construct)? What should the return types look like? Or are the pointers with nullptr checks most suitable here?

? GetAttachment() {return DoGetAttachment();}
2

There are 2 answers

0
joergbrech On BEST ANSWER

The answer really boils down to what you expect of the DoGetAttachment function. Should it "forward the optionalness" of the member to the caller, together with the responsibility of checking if the value exists? Or should the DoGetAttachment function return a reference if the stored std::optional has a value and throw an exception if it doesn't?

Assuming you want to "forward the optionalness" to the caller of the function, you basically have two options.

Option 1: Use (raw) pointers

Use pointers, as you already have done according to your question. It is the most simple solution and standard practice.

Option 2: Use std::optional together with std::reference_wrapper

std::optional is not polymorphic, so you cannot cast a std::optional<Derived> to an std::optional<Base>. What you can do is return an std::optional<Base*> or an std::optional<std::reference_wrapper<Base>>. IMHO, I would prefer std::reference_wrapper, because otherwise you have the "optionalness" property on two levels: Once for the std::optional and once for the stored pointer. So you need to check twice for existence.

Option 2 introduces some additional complexity to your code and it is up to you to decide if it is worth it.

Arguments for using std::optional are, that

  • the "optionalness" of the return value is communicated through semantics. A returned pointer doesn't automatically convey that information;
  • returning an std::optional allows you to use the monadic operators of std::optional, such as value_or, and_then, transform (some of which are C++23 features); and that
  • in contrast to owning (raw) pointers, std::optionals do not perform any dynamic memory allocation (if that is something that you need).

Here is an implementation, where DoGetAttachment returns std::optional<std::reference_wrapper<IToolAttachment>>.

#include <optional>
#include <iostream>

struct IAttachment {
    virtual ~IAttachment(){}
    virtual std::string foo() const = 0;
};

struct Tool1Attachment : IAttachment 
{ 
    std::string foo() const override { return "Tool1Attachment"; }; 
};
struct Tool2Attachment : IAttachment
{ 
    std::string foo() const override { return "Tool2Attachment"; }; 
};

struct ITool {
  virtual ~ITool(){}

  using IAttachmentCRef = std::reference_wrapper<IAttachment const>;
  virtual std::optional<IAttachmentCRef> DoGetAttachment() const = 0;

};

template <typename T>
struct Tool: public ITool {
  std::optional<T> opt_attachment;

  std::optional<IAttachmentCRef> DoGetAttachment() const override
  {
    return opt_attachment.transform([](auto& v){ return std::ref(v); });
  }
  
};

using Tool1 = Tool<Tool1Attachment>;
using Tool2 = Tool<Tool2Attachment>;

int main()
{
    auto print = [](ITool const& tool) {
        return tool.DoGetAttachment()
            .transform([](auto v){ return v.get().foo(); })
            .value_or("No value.");
    };

    Tool1 tool1;

    Tool2 tool2;
    tool2.opt_attachment = Tool2Attachment();
    
    std::cout << print(tool1) << "\n";
    std::cout << print(tool2) << "\n";
}

Output:

No value.
Tool2Attachment

Live Code Demo

4
James Kanze On

A pointer is the obvious solution (with a null pointer if the optional is empty). If you're using inheritance, you'll need a pointer (or a reference) anyway, in order for polymorphism to work, and C++ support covariant return types, so that Owner1 can return a Owned1*. And checking for null on a returned pointer is pretty much standard practice. The alternative would be to return a optional<IOwned*> (since covariance won't work here), but I don't see what that would buy you except additional complexity.