0

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

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. 清除容器,然后循环重新开始。
4

5 回答 5

8

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函数进行的,那么保存容器的代码不需要知道可以存储的类型的完整列表。

于 2020-01-17T09:27:00.407 回答
3

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

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

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

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

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

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

于 2020-01-17T09:28:53.053 回答
2

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

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

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

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

我对表演也很感兴趣。

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

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

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

于 2020-01-17T09:33:48.087 回答
2

评论中提到了通过 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);
} 
于 2020-01-17T10:34:37.443 回答
1

这是不一样的。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

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

于 2020-01-17T09:23:21.870 回答