Вопрос или проблема
У меня есть класс хранения, который хранит указатели void
(я знаю, что это спорный дизайн, но я его не разрабатывал) на произвольные типы данных. При написании модульного теста я заметил, что получаю ошибку сегментации, когда данные возвращаются из хранилища. Используя отладчик MS Visual, я заметил, что значения в возвращаемом указателе все недопустимы. Кто-нибудь знает, почему? Это происходит только в том случае, если B виртуально унаследован от A. Есть ли способ заставить извлечение работать?
#include <iostream>
#include <compare>
#include <string>
#include <unordered_map>
#include <typeindex>
#include <memory>
struct MapKey final
{
std::string name;
std::type_index type;
};
std::strong_ordering operator<=>(const MapKey& lhs, const MapKey& rhs)
{
if (lhs.type < rhs.type)
{
return std::strong_ordering::less;
}
if (lhs.type > rhs.type)
{
return std::strong_ordering::greater;
}
return lhs.name <=> rhs.name;
};
bool operator==(const MapKey& lhs, const MapKey& rhs)
{
return (lhs <=> rhs) == std::strong_ordering::equal;
}
template <typename D>
MapKey makeKey(D* data = nullptr)
{
auto const& type = (data == nullptr) ? typeid(D) : typeid(*data);
std::string typen{type.name()};
return MapKey{typen, type};
}
struct Hasher final
{
auto operator()(const MapKey& key) const -> size_t
{
return std::hash<std::type_index>{}(key.type) ^ std::hash<std::string>{}(key.name);
}
};
class DataMap
{
public:
template <typename D>
void setData(std::shared_ptr<D> spData)
{
m_dataMap[makeKey<D>(spData.get())] = spData;
}
template <typename D>
[[nodiscard]] std::shared_ptr<D> getData() const
{
auto const data = m_dataMap.find(makeKey<D>());
if (data != m_dataMap.end())
{
return std::static_pointer_cast<D>(data->second);
}
return nullptr;
}
private:
using DATA_MAP = std::unordered_map<MapKey, std::shared_ptr<void>, Hasher>;
DATA_MAP m_dataMap;
};
class A
{
public:
virtual size_t size() const
{
return 1;
}
};
class B : virtual public A
{
public:
size_t size() const override
{
return 2;
}
};
int main()
{
std::shared_ptr<A> spA = std::make_shared<B>(); // базовый указатель (требование)
auto k1 = makeKey<A>(spA.get());
auto k2 = makeKey<B>();
auto h1 = Hasher()(k1);
auto h2 = Hasher()(k2);
auto const& tAB = typeid(*spA);
auto const& tB = typeid(B);
if (tAB != tB) {
std::cout << "Разные типы для tAB & tB!"<< std::endl;
}
if (h1 != h2) {
std::cout << "Разные хеши!"<< std::endl;
}
DataMap ds;
ds.setData(spA);
auto spGet = ds.getData<B>();
auto const& tGet = typeid(*spGet); // Ошибка сегментации
if (tGet != tB) {
std::cout << "Разные типы для tGet & tB!"<< std::endl;
}
if (spGet.get() != spA.get()) {
std::cout << "Разные указатели!"<< std::endl;
}
if (spGet->size() != 2) {
std::cout << "Разные размеры!"<< std::endl;
}
}
Пытался не использовать виртуальное наследование для B. Указатель возвращался корректно.
Ответ или решение
Ваша проблема связана с использованием виртуального наследования в C++. Поскольку ваш код включает в себя класс A
, от которого виртуально наследуется класс B
, это создает ситуации, которые могут привести к неправильной обработке указателей, что, в свою очередь, может вызвать неопределенное поведение или даже сбой программы.
Понимание Виртуального Наследования
Виртуальное наследование используется для устранения проблем "ромбовидного" наследования, когда один и тот же базовый класс может быть наследован через несколько путей (линей, не создавая дублирование). Когда класс B
виртуально наследует класс A
, компилятор гарантирует, что существует только один экземпляр A
в дереве наследования, даже если B
наследуется от других классов. Это означает, что для корректной работы с виртуальным наследованием необходимы дополнительные механизмы управления памятью.
Проблема с std::shared_ptr
При использовании std::shared_ptr
для хранения указателя на объект A
, в то время как фактически объект имеет тип B
, могут возникнуть проблемы при попытке извлечь std::shared_ptr<B>
. Ваш код в методе getData
использует std::static_pointer_cast
, что не является подходящим, когда есть виртуальное наследование.
При виртуальном наследовании, на самом деле, объект A
, который доступен из shared_ptr<B>
, может не совпадать с объектом A
, который хранится в контейнере DataMap
, поскольку виртуальные базовые классы имеют особую организацию в памяти. Следовательно, попытка получить доступ к методу size()
будет приводить к сегментации (segfault), так как память, на которую указывает указатель, не соответствует ожидаемой.
Решение проблемы
Для устранения данной проблемы вам нужно использовать более подходящий механизм извлечения указателей. Вместо std::static_pointer_cast
, вам следует использовать std::dynamic_pointer_cast
, который обеспечит безопасное преобразование типов с проверкой, соответствуют ли фактический объект и запрашиваемый тип один и тот же.
Вот как вы можете изменить метод getData
в классе DataMap
:
template <typename D>
[[nodiscard]] std::shared_ptr<D> getData() const
{
auto const data = m_dataMap.find(makeKey<D>());
if (data != m_dataMap.end())
{
// Используем dynamic_pointer_cast для безопасного извлечения
return std::dynamic_pointer_cast<D>(data->second);
}
return nullptr;
}
Примечания
-
Использование
dynamic_pointer_cast
: Данная функция будет выполнять проверку во время выполнения на соответствие типа. Если тип не будет подходящим, она вернетnullptr
вместо того, чтобы вызывать сбой. -
Единообразие хранения: Убедитесь, что вы правильно храните и извлекаете указатели в своем контейнере. Это особенно важно, когда вы имеете дело с виртуальным наследованием.
-
*Избегайте использования `void
**: Хранение
void*` указателей может скрыть ошибки типов и затруднить отладку. По возможности старайтесь использовать конкретные типы.
Следуя этим рекомендациям, вы сможете разобраться с проблемой "мусорных значений" в вашей программе и избежать дальнейших сбоев в работе кода.