std::variant 与 C++ 中异构容器的指向基 class 的指针

std::variant vs pointer to base class for heterogeneous containers in C++

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

class BaseClass {
public:
  int x;
}

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

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

我想为这些 class 制作一个异构容器。由于 subclasses 是从基础 class 派生的,我可以做这样的事情:

std::vector<BaseClass*> container1;

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

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

使用一个或另一个有什么advantages/disadvantages?我也对表演感兴趣

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

  1. 装满容器,
  2. x
  3. 排序
  4. 遍历所有元素,找出类型,相应地使用它,
  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 风格联合体的要点是能够访问它在同一内存位置的所有成员。

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

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

std::variant is that you need to specify a list of allowed types; if you add a future derived class you would have to add it to the type list. If you need a more dynamic implementation, you can look at std::any 有问题;我相信它可以达到目的。

I also need to be able to find out the exact type of the elements.

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

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

Take into consideration that, I am going to sort the container by x

在这种情况下,您声明了变量 public,因此排序完全没有问题;您可能需要考虑声明变量 protected 或在基 class.

中实现排序机制

What are the advantages/disadvantages of using one or the other?

与advantages/disadvantages一样,使用指针进行运行时类型解析,使用模板进行编译时类型解析。有很多东西你可以比较。例如:

  • 对于指针,如果滥用它们可能会导致内存冲突
  • 运行时解析有额外的开销(但也取决于你如何使用这个 classes,如果它是虚函数调用,或者只是普通成员字段访问)

但是

  • 指针有固定的大小,可能比你的 class 对象小,所以如果你打算经常复制你的容器可能会更好

I am interested in the performance too.

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

Take into consideration that, I am going to sort the container by x and I also need to be able to find out the exact type of the elements.

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

评论中提到了通过 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);
  }
};

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

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);
}