Указатель выдает мусорные значения при использовании виртуального наследования.

Вопрос или проблема

У меня есть класс хранения, который хранит указатели 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;
}

Примечания

  1. Использование dynamic_pointer_cast: Данная функция будет выполнять проверку во время выполнения на соответствие типа. Если тип не будет подходящим, она вернет nullptr вместо того, чтобы вызывать сбой.

  2. Единообразие хранения: Убедитесь, что вы правильно храните и извлекаете указатели в своем контейнере. Это особенно важно, когда вы имеете дело с виртуальным наследованием.

  3. *Избегайте использования `void**: Хранениеvoid*` указателей может скрыть ошибки типов и затруднить отладку. По возможности старайтесь использовать конкретные типы.

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

Оцените материал
Добавить комментарий

Капча загружается...