【问题标题】:std::variant vs pointer to base class for heterogeneous containers in C++std::variant vs 指向 C++ 中异构容器基类的指针
【发布时间】:2020-01-17 09:13:19
【问题描述】:

让我们假设下面这个类层次结构。

class BaseClass {
public:
  int x;
}

class SubClass1 : public BaseClass {
public:
  double y;
}

class SubClass2 : public BaseClass {
public:
  float z;
}
...

我想为这些类创建一个异构容器。由于子类是从基类派生的,我可以做这样的事情:

std::vector<BaseClass*> container1;

但从 C++17 开始,我也可以像这样使用std::variant

std::vector<std::variant<SubClass1, SubClass2, ...>> container2;

使用其中一种的优点/缺点是什么?我也对表演感兴趣。

考虑到我将按x 对容器进行排序,并且我还需要能够找出元素的确切类型。我要去

  1. 装满容器,
  2. x排序,
  3. 遍历所有元素,找出类型,相应地使用它,
  4. 清空容器,然后循环重新开始。

【问题讨论】:

  • container1 - 遍历容器并具有子类行为,在基类中至少有一个虚函数(可能是析构函数)。 container2- 更多类似类型的安全联合,其中一个对象是可见的。
  • std::visit of std::variant 会调用类似于虚拟调度。
  • @Sylvester 你如何使用它们?我的意思是,您是否在每个元素上调用虚函数?最佳解决方案取决于用例。请用一个例子更新问题。
  • "找出类型,使用它" 你可以在基类中声明虚函数。那可能会更干净,但如果没有更多信息,很难说什么会更干净。
  • 您可以在基类中声明一个虚函数sendData 并为每个子类覆盖它。没有必要明确地“找出类型”。

标签: c++ stl containers c++17 heterogeneous


【解决方案1】:

std::variant<A,B,C> 包含一组封闭类型中的一个。您可以使用std::holds_alternative 检查它是否拥有给定的类型,或者使用std::visit 传递带有重载operator() 的访问者对象。可能没有动态内存分配,但是很难扩展:带有std::variant 的类和任何访问者类都需要知道可能的类型列表。

另一方面,BaseClass* 拥有一组无限的派生类类型。您应该持有std::unique_ptr<BaseClass>std::shared_ptr<BaseClass> 以避免内存泄漏的可能性。要确定是否存储了特定类型的实例,您必须使用dynamic_castvirtual 函数。此选项需要动态内存分配,但如果所有处理都是通过virtual 函数进行的,那么保存容器的代码不需要知道可以存储的类型的完整列表。

【讨论】:

  • 抱歉,有点跑题了,但使用动态内存分配真的有必要吗?它可能是指向堆栈分配对象的指针(在这种情况下没有智能指针)。也许这只是一种罕见的情况,如果向量应该拥有实例,则无法动态分配它们
  • 是的,您可以将指针指向堆栈上分配的现有对象或作为全局对象,但这种情况非常罕见。
【解决方案2】:

std::variant 的一个问题是你需要指定一个允许的类型列表;如果您添加未来派生类,则必须将其添加到类型列表中。如果需要更动态的实现,可以看std::any;我相信它可以达到目的。

我还需要能够找出元素的确切类型。

对于类型识别,您可以创建类似instanceof 的模板,如C++ equivalent of instanceof 所示。也有人说,使用这种机制的需要有时会暴露出糟糕的代码设计。

性能问题不是可以提前检测到的,因为它取决于使用情况:这是一个测试不同实现的问题,看看哪个更快。

考虑到这一点,我将按x对容器进行排序

在这种情况下,您声明变量public,因此排序完全没有问题;您可能需要考虑声明变量protected 或在基类中实现排序机制。

【讨论】:

  • 我指定允许的类型没有问题。它将是 4 个子类,并且在可预见的将来不会改变。
  • 在这种情况下,这是一个不错的选择,但一般情况下,代码将来需要更改/改进,并且可能会被原始设计人员以外的其他人更改,因此创建更简单的设计总是一件好事。还有@AntonyWilliams 指出的内存管理问题。
【解决方案3】:

使用其中一种有什么优点/缺点?

与使用指针进行运行时类型解析和使用模板进行编译时类型解析的优点/缺点相同。有很多东西可以比较。例如:

  • 如果滥用指针,可能会导致内存冲突
  • 运行时解析有额外的开销(但也取决于您将如何准确使用这些类,如果是虚函数调用,还是只是普通成员字段访问)

但是

  • 指针具有固定大小,并且可能比您的类的对象要小,因此如果您计划经常复制容器可能会更好

我也对表演感兴趣。

然后只需衡量您的应用程序的性能,然后再决定。推测哪种方法可能更快并不是一个好习惯,因为它在很大程度上取决于用例。

考虑到这一点,我将按 x 对容器进行排序 而且我还需要能够找出元素的确切类型。

在这两种情况下,您都可以找出类型。 dynamic_cast 在指针的情况下,holds_alternativestd::variant 的情况下。对于std::variant,必须明确指定所有可能的类型。在这两种情况下访问成员字段x 几乎相同(指针是指针解引用+成员访问,变体是get + 成员访问)。

【讨论】:

    【解决方案4】:

    在 cmets 中提到了通过 TCP 连接发送数据。在这种情况下,使用虚拟调度可能最有意义。

    class BaseClass {
    public:
      int x;
    
      virtual void sendTo(Socket socket) const {
        socket.send(x);
      }
    };
    
    class SubClass1 final : public BaseClass {
    public:
      double y;
    
      void sendTo(Socket socket) const override {
        BaseClass::sendTo(socket);
        socket.send(y);
      }
    };
    
    class SubClass2 final : public BaseClass {
    public:
      float z;
    
      void sendTo(Socket socket) const override {
        BaseClass::sendTo(socket);
        socket.send(z);
      }
    };
    

    然后你可以将指向基类的指针存储在一个容器中,并通过基类来操作对象。

    std::vector<std::unique_ptr<BaseClass>> container;
    
    // fill the container
    auto a = std::make_unique<SubClass1>();
    a->x = 5;
    a->y = 17.0;
    container.push_back(a);
    auto b = std::make_unique<SubClass2>();
    b->x = 1;
    b->z = 14.5;
    container.push_back(b);
    
    // sort by x
    std::sort(container.begin(), container.end(), [](auto &lhs, auto &rhs) {
      return lhs->x < rhs->x;
    });
    
    // send the data over the connection
    for (auto &ptr : container) {
      ptr->sendTo(socket);
    } 
    

    【讨论】:

      【解决方案5】:

      不一样。 std::variant 就像一个具有类型安全性的联合。同一时间最多只能看到一个成员。

      // C++ 17
      std::variant<int,float,char> x;
      x = 5; // now contains int
      int i = std::get<int>(v); // i = 5;
      std::get<float>(v); // Throws
      

      另一个选项基于继承。根据您拥有的指针,所有成员都可见。

      您的选择将取决于您是否希望所有变量都可见以及您想要什么错误报告。

      相关:不要使用指针向量。使用shared_ptr的向量。

      无关:我有点不支持新的 union 变体。旧的 C 风格联合的重点是能够访问它在同一内存位置拥有的所有成员。

      【讨论】:

      • 您说的“不能同时看到一个以上的成员”是什么意思?
      • 如果你给它分配一个整数,你就不能得到很长的结果。这与原来的 C 联合不同。
      猜你喜欢
      • 2016-02-19
      • 2023-04-01
      • 2018-03-24
      • 1970-01-01
      • 2015-08-12
      • 2021-01-12
      • 2011-12-12
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多