从抽象类派生的类中获取从抽象类派生的类的可选项

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

提问人:bmetz 提问时间:11/9/2023 最后编辑:bmetz 更新时间:11/10/2023 访问量:84

问:

我有类似于下面的代码的东西,其中一组具有相似共享行为的类(Tool1、Tool2)都继承自抽象类(ITool)。所有这些类都拥有自己的相应类(Tool1Attachment、Tool2Attachment)的可选类,该类也继承自抽象类 (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 {
  [...]
};

工具类具有可选是有意义的 - 在上下文中,它可能在任何给定时间具有也可能没有实际实例。

如果我有一个 IAttachment,并且想获得 IAttachment,就会出现一个问题。

我最初为此使用了指针,即 ,它“工作正常”。但是,它似乎丢失了 optional 提供的一些对缺失值的更明确的检查,从而使将来更容易遇到 nullptr 问题。IAttachment* GetAttachment();

我还尝试了带有可选的不同结构(std::reference_wrapper),但一直遇到无效的协变错误。

有没有一种方法可以允许返回可选(或类似的结构)?返回类型应该是什么样子的?或者带有 nullptr 检查的指针在这里最合适吗?

? GetAttachment() {return DoGetAttachment();}
C++ 继承 多态性 std可选

评论

2赞 Pepijn Kramer 11/9/2023
也许使用 std::unique_ptr 是更好的模型?也可能是我仍然不明白你要建模什么
0赞 463035818_is_not_an_ai 11/9/2023
应该返回什么?一份副本?参考?还是应该允许转让所有权?DoGetOwned
0赞 463035818_is_not_an_ai 11/9/2023
是故意的吗?DoGetOwnedconst
2赞 Yksisarvinen 11/9/2023
但是,它似乎丢失了 optional 提供的一些对缺失值的更明确的检查我不确定你这是什么意思。检查 in 指针与检查 in(忽略所有权语义)非常相似。nullptrstd::nulloptstd::optional
1赞 bmetz 11/9/2023
@Yksisarvinen我只是说,对于 optional,在显式获取/检查/取消引用值之前,您不能使用包含的值,而使用指向值的指针,您可以立即使用 -> 调用方法,这在编译时不会抱怨。

答:

2赞 James Kanze 11/9/2023 #1

指针是显而易见的解决方案(如果可选为空,则指针为空)。如果使用继承,则无论如何都需要指针(或引用)才能使多态性起作用,并且 C++ 支持协变返回类型,以便可以返回 .在返回的指针上检查 null 几乎是标准做法。另一种选择是返回一个(因为协方差在这里不起作用),但除了额外的复杂性之外,我看不出这会给你带来什么。Owner1Owned1*optional<IOwned*>

评论

0赞 Yksisarvinen 11/9/2023
std::optional<std::reference_wrapper<IOwned>>将是一种可能性,但 IMO 它没有添加任何内容来更喜欢它而不是指针
1赞 joergbrech 11/9/2023
@Yksisarvinen 使用reference_wrapper将增加使用一元运算或在 C++23 中引入的单子运算来链接函数的可能性。此外,不使用动态内存分配。最后,“在返回的指针上检查 null 几乎是标准做法”并不是一个很好的论据恕我直言:我想知道有多少错误是由未经检查的指针引起的。使用 allone 并不能防止它,但至少在语义上很清楚该值可能不存在。and_thenvalue_ortransformstd::optionalstd::optionalstd::optional
0赞 James Kanze 11/10/2023
@joergbrech 返回指针也不需要动态内存分配,因为该成员仍是 .无论你返回的是指针还是某种可选的指针,你都必须检查有效性。(事实上,它是一个指针,而不是一个引用,这使得它在语义上清楚地表明该值可能不存在。std::optional
0赞 joergbrech 11/10/2023
@JamesKanze,我同意,我也同意你的 anwer(+1 来自我)。我只是想指出这两种方法之间的一些差异,这两种方法都有优点和缺点。根据 OP 的说法,存在一种不基于指针的替代实现。拥有(智能)指针确实需要动态内存分配。是的,指针应该始终暗示该值可能不存在,您必须在取消引用之前进行检查。但是人们出于不同的原因使用指针。此外,在场景中,人们会期望从上下文中获取值。这就是错误发生的地方。std::optional
0赞 joergbrech 11/9/2023 #2

答案实际上归结为您对该功能的期望。它是否应该将成员的可选性“转发”给调用者,并负责检查值是否存在?或者,如果存储的函数有值,则函数是否应该返回引用,如果没有,则抛出异常?DoGetAttachmentDoGetAttachmentstd::optional

假设您要将可选性“转发”给函数的调用者,您基本上有两个选项。

选项 1:使用(原始)指针

使用指针,就像您已经根据自己的问题所做的那样。这是最简单的解决方案和标准做法。

选项 2:与std::optionalstd::reference_wrapper

std::optional不是多态的,因此不能将 a 转换为 .您可以做的是返回 an 或 .恕我直言,我更喜欢 ,因为否则您在两个级别上具有“可选性”属性:一次用于存储的指针,一次用于存储的指针。因此,您需要检查两次是否存在。std::optional<Derived>std::optional<Base>std::optional<Base*>std::optional<std::reference_wrapper<Base>>std::reference_wrapperstd::optional

选项 2 会给您的代码带来一些额外的复杂性,由您决定是否值得。

使用的参数是,那std::optional

  • 返回值的“可选性”是通过语义传达的。返回的指针不会自动传达该信息;
  • 返回 an 允许您使用 的 一元运算符,例如 、 、(其中一些是 C++23 功能);并且std::optionalstd::optionalvalue_orand_thentransform
  • 与拥有(原始)指针相比,S 不执行任何动态内存分配(如果需要的话)。std::optional

这是一个实现,其中返回 .DoGetAttachmentstd::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";
}

输出:

No value.
Tool2Attachment

实时代码演示