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();}
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.
Use pointers, as you already have done according to your question. It is the most simple solution and standard practice.
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
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 thatstd::optional
s 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