将 std::map<int, std::shared_ptr<Base>> 转换为 std::map<int, std::shared_ptr<Derived>> 的最有效安全方法
Most efficient safe way to cast std::map<int, std::shared_ptr<Base>> to std::map<int, std::shared_ptr<Derived>>
我们目前存储了几个不同的数据模型集合,如下所示:
std::map<std::string, std::map<int64_t, std::shared_ptr<DataObject>>> models;
字符串映射到已知的类型列表,这全部由序列化处理。嵌套映射包含 "Object Id" 和关联(反序列化)std::shared_ptr
的集合
DataObject 是一个基类 class,我们有几种派生自它的类型。
我们有一个方法可以获取给定类型的所有数据对象:
static std::map<int64_t, std::shared_ptr<DataObject>> *getAll(std::string type);
这只是 returns 在给定 "type" 键处指向地图的指针。
今天我遇到了代码审查以添加以下内容,我认为它调用了 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;
}
像这样转换的好处是它不涉及循环或复制(据我所知),它是一个简单的指针转换。这太妙了。但我不认为它是便携的或安全的,虽然我不知道最好的便携和安全的替代品是什么。
我在下面有这个问题的玩具版的简化版本,它确实按预期吐出正确的输出:
#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;
}
我的问题是是否有一种不涉及大量复制的好方法,或者至少是否有一种干净的方法来做到这一点。我们目前正在使用 C++17。
嗯,不能这么随便施法。 std::map<int64_t, std::shared_ptr<M>>
和 std::map<int64_t, std::shared_ptr<DataObject>>
是完全不同的类型。您不能在不触发 UB 的情况下将指针指向不相关的 class。你必须以某种方式制作副本。
如果要将指向派生 class 的指针转换为指向基 class 的指针,您可以直接执行以下操作:
std::map<int64_t, std::shared_ptr<M>> new_map{old_map.begin(), old_map.end()};
这利用了 shared_ptr
在需要时进行转换这一事实。如果你反过来做,那么你可以依赖隐式转换,所以使用 static_pointer_cast
.
您不能直接使用带一对迭代器的 std::map 构造函数,因为转换试图从 Base 转换为 Derived,这不能隐式完成,但您可以 运行 std::transform 安全。这确实涉及复制,但具有 不是 未定义行为的好处。
template <typename M>
static std::map<int64_t, std::shared_ptr<M>> getAll(const std::string &type) {
auto* source = getAll(type);
std::map<int64_t, std::shared_ptr<M>> castMap;
std::transform(source->begin(), source->end(), std::inserter(castMap, castMap.end()), [](auto& kv) {
return std::pair<const int, std::shared_ptr<M>>(kv.first, std::static_pointer_cast<M>(kv.second));
});
return castMap;
}
它不会提供完全相同的接口,但想到的一个类似但更安全的想法是使用 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);
}
那么你的例子 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;
}
这避免了创建任何第二个容器对象,并且在正确使用时不涉及未定义的行为。使用转换迭代器将主要导致与不安全转换相同的机器代码,除了取消引用确实创建一个新的 shared_ptr<Derived>
对象,这将涉及一些引用计数开销。 See the full working program on coliru.
除了使用上面基于范围的 for
中所示的 make_downcast_pair_range<Derived>(some_map)
之外, make_downcast_pair_iterator<Derived>
还可以直接用于获取转换迭代器用于其他目的,例如从结果地图的 find(k)
。并且给定一个转换迭代器,您可以使用 iter.base()
返回到真实地图的迭代器,例如传递给地图的 erase(iter)
.
当然,如果指针实际上没有指向 Derived
对象,那么使用 std::static_pointer_cast
的结果仍然是未定义的行为。如果担心有人在获取对象时可能会使用错误的 "derived" 模板参数,或者映射可能最终包含指向错误派生对象类型的指针,您可以更改 downcast_pair_iterator<D, I>::convert
私有函数以使用std::dynamic_pointer_cast
相反,如果结果是空指针,则抛出或中止。
我们目前存储了几个不同的数据模型集合,如下所示:
std::map<std::string, std::map<int64_t, std::shared_ptr<DataObject>>> models;
字符串映射到已知的类型列表,这全部由序列化处理。嵌套映射包含 "Object Id" 和关联(反序列化)std::shared_ptr
DataObject 是一个基类 class,我们有几种派生自它的类型。
我们有一个方法可以获取给定类型的所有数据对象:
static std::map<int64_t, std::shared_ptr<DataObject>> *getAll(std::string type);
这只是 returns 在给定 "type" 键处指向地图的指针。
今天我遇到了代码审查以添加以下内容,我认为它调用了 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;
}
像这样转换的好处是它不涉及循环或复制(据我所知),它是一个简单的指针转换。这太妙了。但我不认为它是便携的或安全的,虽然我不知道最好的便携和安全的替代品是什么。
我在下面有这个问题的玩具版的简化版本,它确实按预期吐出正确的输出:
#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;
}
我的问题是是否有一种不涉及大量复制的好方法,或者至少是否有一种干净的方法来做到这一点。我们目前正在使用 C++17。
嗯,不能这么随便施法。 std::map<int64_t, std::shared_ptr<M>>
和 std::map<int64_t, std::shared_ptr<DataObject>>
是完全不同的类型。您不能在不触发 UB 的情况下将指针指向不相关的 class。你必须以某种方式制作副本。
如果要将指向派生 class 的指针转换为指向基 class 的指针,您可以直接执行以下操作:
std::map<int64_t, std::shared_ptr<M>> new_map{old_map.begin(), old_map.end()};
这利用了 shared_ptr
在需要时进行转换这一事实。如果你反过来做,那么你可以依赖隐式转换,所以使用 static_pointer_cast
.
您不能直接使用带一对迭代器的 std::map 构造函数,因为转换试图从 Base 转换为 Derived,这不能隐式完成,但您可以 运行 std::transform 安全。这确实涉及复制,但具有 不是 未定义行为的好处。
template <typename M>
static std::map<int64_t, std::shared_ptr<M>> getAll(const std::string &type) {
auto* source = getAll(type);
std::map<int64_t, std::shared_ptr<M>> castMap;
std::transform(source->begin(), source->end(), std::inserter(castMap, castMap.end()), [](auto& kv) {
return std::pair<const int, std::shared_ptr<M>>(kv.first, std::static_pointer_cast<M>(kv.second));
});
return castMap;
}
它不会提供完全相同的接口,但想到的一个类似但更安全的想法是使用 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);
}
那么你的例子 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;
}
这避免了创建任何第二个容器对象,并且在正确使用时不涉及未定义的行为。使用转换迭代器将主要导致与不安全转换相同的机器代码,除了取消引用确实创建一个新的 shared_ptr<Derived>
对象,这将涉及一些引用计数开销。 See the full working program on coliru.
除了使用上面基于范围的 for
中所示的 make_downcast_pair_range<Derived>(some_map)
之外, make_downcast_pair_iterator<Derived>
还可以直接用于获取转换迭代器用于其他目的,例如从结果地图的 find(k)
。并且给定一个转换迭代器,您可以使用 iter.base()
返回到真实地图的迭代器,例如传递给地图的 erase(iter)
.
当然,如果指针实际上没有指向 Derived
对象,那么使用 std::static_pointer_cast
的结果仍然是未定义的行为。如果担心有人在获取对象时可能会使用错误的 "derived" 模板参数,或者映射可能最终包含指向错误派生对象类型的指针,您可以更改 downcast_pair_iterator<D, I>::convert
私有函数以使用std::dynamic_pointer_cast
相反,如果结果是空指针,则抛出或中止。