STD :: вариант против указателя на базовый класс для гетерогенных контейнеров в C ++ - PullRequest
0 голосов
/ 17 января 2020

Давайте предположим, что иерархия классов ниже.

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. Очистите контейнер, затем цикл начнется заново.

Ответы [ 5 ]

6 голосов
/ 17 января 2020

std::variant<A,B,C> содержит один из закрытых наборов типов. Вы можете проверить, содержит ли он данный тип с помощью std::holds_alternative, или использовать std::visit для передачи объекта посетителя с перегруженным operator(). Вероятно, динамическое выделение памяти c отсутствует, однако его сложно расширить: класс с std::variant и любые классы посетителей должны знать список возможных типов.

С другой стороны , BaseClass* содержит неограниченный набор производных типов классов. Вы должны держать std::unique_ptr<BaseClass> или std::shared_ptr<BaseClass>, чтобы избежать потенциальной утечки памяти. Чтобы определить, хранится ли экземпляр указанного типа c, необходимо использовать функцию dynamic_cast или virtual. Эта опция требует динамического выделения памяти c, но если вся обработка выполняется с помощью функций virtual, то коду, в котором хранится контейнер, не нужно знать полный список типов, которые могут быть сохранены.

2 голосов
/ 17 января 2020

Отправка данных по 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);
} 
2 голосов
/ 17 января 2020

Проблема с std::variant заключается в том, что вам нужно указать список разрешенных типов; если вы добавите будущий производный класс, вам придется добавить его в список типов. Если вам нужна более динамичная реализация c, вы можете посмотреть на std::any; Я считаю, что это может служить цели.

Мне также нужно иметь возможность узнать точный тип элементов.

Для распознавания типов вы можете создать шаблон instanceof, как показано в C ++ эквивалент instanceof. Также сказано, что необходимость использования такого механизма иногда выявляет плохой дизайн кода.

Проблема производительности не может быть обнаружена заранее, потому что она зависит от использования: это вопрос тестирования различных реализаций, и вы увидите, что все быстрее.

Примите во внимание, что я собираюсь отсортировать контейнер по x

. В этом случае вы объявляете переменную public, так что сортировка вообще не проблема; Вы можете рассмотреть возможность объявления переменной protected или реализации механизма сортировки в базовом классе.

1 голос
/ 17 января 2020

Каковы преимущества / недостатки использования одного или другого?

То же, что преимущества / недостатки использования указателей для разрешения типов во время выполнения и шаблонов для разрешения типов времени компиляции. Есть много вещей, которые вы можете сравнить. Например:

  • с указателями у вас могут быть нарушения памяти, если вы их неправильно используете
  • разрешение времени выполнения имеет дополнительные накладные расходы (но также зависит от того, как именно вы будете использовать эти классы, если он виртуальный вызов функции или просто общий доступ к полю члена)

, но указатели

  • имеют фиксированный размер и, вероятно, меньше, чем будет объект вашего класса, так что он может будет лучше, если вы планируете часто копировать свой контейнер

Мне тоже интересна производительность.

Затем просто измерьте производительность вашего приложения и затем решите , Не стоит спекулировать, какой подход может быть быстрее, потому что он сильно зависит от варианта использования.

Примите во внимание, что я собираюсь отсортировать контейнер по x, и мне также нужно чтобы иметь возможность узнать точный тип элементов.

В обоих случаях вы можете узнать тип. dynamic_cast в случае указателей, holds_alternative в случае std::variant. С std::variant все возможные типы должны быть явно указаны. Доступ к полю члена x будет почти одинаковым в обоих случаях (с указателем это разыменование указателя + доступ к члену, с вариантом get + доступ к члену).

0 голосов
/ 17 января 2020

Это не то же самое. 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 в стиле состоял в том, чтобы иметь возможность доступа ко всем членам, которые у него были в том же месте памяти.

...