将 std::map<int, std::shared_ptr<Base>> 转换为 std::map<int, std::shared_ptr<Derived>> 的最有效安全方式

M2t*_*2tM 6 c++ casting shared-ptr c++17

我们目前存储了几个不同的数据模型集合,如下所示:

std::map<std::string, std::map<int64_t, std::shared_ptr<DataObject>>> models;
Run Code Online (Sandbox Code Playgroud)

字符串映射到一个已知的类型列表,这都是通过序列化来处理的。嵌套映射包含“对象 ID”和关联(反序列化) std::shared_ptr<DataObject> 的集合

DataObject 是一个基类,我们从它派生了几种类型。

我们有一个方法来获取给定类型的所有数据对象:

static std::map<int64_t, std::shared_ptr<DataObject>> *getAll(std::string type);
Run Code Online (Sandbox Code Playgroud)

这只是在给定的“类型”键处返回一个指向地图的指针。

今天我遇到了一个代码审查来添加以下我认为调用 UB 但似乎可以运行的代码。这让我有点紧张,正在寻找有效的解决方案:

template <typename M>
static std::map<int64_t, std::shared_ptr<M>> *getAll(const std::string &type) {
    auto castObjectMap = reinterpret_cast<std::map<int64_t, std::shared_ptr<M>>*>(getAll(type));
    return castObjectMap;
}
Run Code Online (Sandbox Code Playgroud)

像这样转换的好处是它不涉及循环或复制(据我所知),这是一个简单的指针转换。这太妙了。但我不认为它是便携或安全的,尽管我不知道最好的便携和安全替代品是什么。

我有一个下面这个问题的玩具版本的简化版本,它实际上按预期吐出正确的输出:

#include <iostream>
#include <memory>
#include <string>
#include <map>

using namespace std;

struct Base {
    Base(){}
    virtual ~Base(){}

    Base(int u):x(u){}

    int x = 0;
};

struct Derived : public Base {
    Derived(){}
    Derived(int u, int v):Base(u),y(v){}
    int y = 0;
};

int main() {
    map<int, shared_ptr<Base>> test {
        {0, make_shared<Derived>(2, 3)},
        {1, make_shared<Derived>(4, 5)},
        {2, make_shared<Derived>(6, 7)}
    };

    map<int, shared_ptr<Derived>> *castVersion = reinterpret_cast<map<int, shared_ptr<Derived>>*>(&test);

    for(auto&&kv : *castVersion){
        cout << kv.first << ": " << kv.second->x << ", " << kv.second->y << std::endl;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

我的问题是,是否有一种不涉及大量复制的好方法,或者至少是否有一种干净的方法可以做到这一点。我们目前使用的是 C++17。

asc*_*ler 5

它不会提供完全相同的接口,但我想到的一个类似但更安全的想法是使用boost::transform_iterator创建迭代器来透明地处理shared_ptr映射内指针的转换。

#include <memory>
#include <utility>
#include <type_traits>
#include <boost/iterator/transform_iterator.hpp>

template <class Derived, class Iterator>
class downcast_pair_iterator
    : public boost::transform_iterator<
        std::pair<
            typename std::iterator_traits<Iterator>::value_type::first_type,
            const std::shared_ptr<Derived>
        > (*)(Iterator),
        Iterator>
{
public:
    using base_value_type = typename std::iterator_traits<Iterator>::value_type;
    using key_type = const typename base_value_type::first_type;
    using base_mapped_type = typename base_value_type::second_type;
    using mapped_type = const std::shared_ptr<Derived>;
    using value_type = std::pair<key_type, mapped_type>;

private:
    template <typename T>
    static T* shared_to_raw(const std::shared_ptr<T>&); // undefined
    static_assert(std::is_base_of_v<
        std::remove_pointer_t<
            decltype(shared_to_raw(std::declval<base_mapped_type&>()))>,
        Derived>);

    static value_type convert(const base_value_type& pair_in)
    {
        return value_type(pair_in.first,
            std::static_pointer_cast<Derived>(pair_in.second));
    }
public:
    explicit downcast_pair_iterator(Iterator iter)
        : transform_iterator(iter, &convert) {}
};

template <class Derived, class Iterator>
auto make_downcast_pair_iter(Iterator iter)
{
    return downcast_pair_iterator<Derived, Iterator>(iter);
}

template <class Derived, class Range>
class downcast_pair_range
{
public:
    explicit downcast_pair_range(Range& c)
        : source_ref(c) {}

    auto begin() const {
        using std::begin;
        return make_downcast_pair_iter<Derived>(begin(source_ref));
    }
    auto end() const {
        using std::end;
        return make_downcast_pair_iter<Derived>(end(source_ref));
    }

private:
    Range& source_ref;
};

template <class Derived, class Range>
auto make_downcast_pair_range(Range& r)
{
    return downcast_pair_range<Derived, Range>(r);
}
template <class Derived, class Range>
auto make_downcast_pair_range(const Range &r)
{
    return downcast_pair_range<Derived, const Range>(r);
}
Run Code Online (Sandbox Code Playgroud)

那么你的例子main可能会变成:

int main() {
    std::map<int, std::shared_ptr<Base>> test {
        {0, std::make_shared<Derived>(2, 3)},
        {1, std::make_shared<Derived>(4, 5)},
        {2, std::make_shared<Derived>(6, 7)}
    };

    for (auto&& kv : make_downcast_pair_range<Derived>(test)){
        std::cout << kv.first << ": "
                  << kv.second->x << ", " << kv.second->y << std::endl;
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这避免了创建任何第二个容器对象,并且在正确使用时不会涉及未定义的行为。使用转换迭代器通常会产生与不安全转换相同的机器代码,只是取消引用确实会创建一个新shared_ptr<Derived>对象,这将涉及一些引用计数开销。 请参阅有关coliru 的完整工作程序。

除了上面make_downcast_pair_range<Derived>(some_map)基于范围的使用for之外,make_downcast_pair_iterator<Derived>还可以直接用于获取转换迭代器以用于其他目的,例如从地图的find(k). 并且给定一个转换迭代器,您可以使用 返回到真实地图的迭代器iter.base(),例如传递给地图的erase(iter).

当然,std::static_pointer_cast如果指针实际上并未指向Derived对象,则使用 的结果仍然是未定义的行为。如果有人担心在获取对象时可能使用错误的“派生”模板参数,或者映射可能以某种方式最终包含指向错误派生对象类型的指针,您可以更改downcast_pair_iterator<D, I>::convert私有函数以使用std::dynamic_pointer_cast,然后抛出或中止如果结果是一个空指针。