【发布时间】:2020-05-19 10:50:57
【问题描述】:
我在某个模块的界面中有如下功能:
void DoSomething(Span<MyObject *const> objects);
,其中Span 是我对C++20 的std::span 模板的简化实现。
这个函数只是迭代一个指向对象的连续指针序列并调用它们的一些函数,而不试图修改指针(因此签名中的const)。
在来电者方面,我有一个std::vector<std::unique_ptr<MyObject>>。我想将该向量传递给DoSomething 函数而不分配额外的内存(对于任何像临时的std::vector<MyObject*>)。我只想在恒定时间内将unique_ptrs 的左值向量转换为不可变原始指针的Span。
这一定是可能的,因为带有无状态删除器的std::unique_ptr<T> 与原始T* 指针具有相同的大小和对齐方式,并且它存储在内部的只是原始指针本身。因此,按字节计算,std::vector<std::unique_ptr<MyObject>> 必须与 std::vector<MyObject*> 具有相同的表示形式——因此必须可以将其传递给需要 Span<MyObject *const> 的函数。
我的问题是:
在
std::span的当前提案中是否可以进行这样的转换,而不会导致未定义的行为并依赖于肮脏的黑客攻击?如果不是,是否可以在以下标准(例如 C++23)中预期?
使用我在我的
Span版本中实现的转换,使用memcpy的肮脏技巧有什么危险?它在实践中似乎工作正常,但我想其中可能存在一些未定义的行为。如果有,在哪些情况下,这种未定义的行为会在 MSVC、GCC 或 Clang/LLVM 上让我措手不及,具体情况如何?如果可能的话,我将不胜感激。
我的代码是这样的:
namespace detail
{
constexpr std::size_t dynamic_extent = static_cast<std::size_t>(-1);
template<typename SourceSmartPointer, typename SpanElement, typename = void>
struct is_smart_pointer_type_compatible_impl
: std::false_type
{
};
template<typename SourceSmartPointer, typename SpanElement>
struct is_smart_pointer_type_compatible_impl<SourceSmartPointer, SpanElement,
decltype((void)(std::declval<SourceSmartPointer&>().get()))>
: std::conjunction<
std::is_pointer<SpanElement>,
std::is_const<SpanElement>,
std::is_convertible<std::add_pointer_t<decltype(std::declval<SourceSmartPointer&>().get())>,
SpanElement*>,
std::is_same<std::remove_cv_t<std::remove_pointer_t<decltype(std::declval<SourceSmartPointer&>().get())>>,
std::remove_cv_t<std::remove_pointer_t<SpanElement>>>,
std::bool_constant<(sizeof(SourceSmartPointer) == sizeof(SpanElement)) &&
(alignof(SourceSmartPointer) == alignof(SpanElement))>>
{
};
// Helper type trait which detects whether a contiguous range of smart pointers of the source type
// can be used to initialize a span of respective immutable raw pointers using a memcpy-based hack.
template<typename SourceSmartPointer, typename SpanElement>
struct is_smart_pointer_type_compatible
: is_smart_pointer_type_compatible_impl<SourceSmartPointer, SpanElement>
{
};
template<typename T, typename R>
inline T* cast_smart_pointer_range_data_to_raw_pointer(R& source_range)
{
T* result = nullptr;
auto* source_range_data = std::data(source_range);
std::memcpy(&result, &source_range_data, sizeof(T*));
return result;
}
}
template<typename T, std::size_t Extent = detail::dynamic_extent>
class Span final
{
public:
// ...
// Non-standard extension.
// Allows, e.g., to convert `std::vector<std::unique_ptr<Object>>` to `Span<Object *const>`
// by using the fact that such smart pointers are bytewise equal to the resulting raw pointers;
// `const` is required on the destination type to ensure that the source smart pointers
// will be read-only for the users of the resulting Span.
template<typename R,
std::enable_if_t<std::conjunction<
std::bool_constant<(Extent == detail::dynamic_extent)>,
detail::is_smart_pointer_type_compatible<std::remove_reference_t<decltype(*std::data(std::declval<R&&>()))>, T>,
detail::is_not_span<R>,
detail::is_not_std_array<R>,
std::negation<std::is_array<std::remove_cv_t<std::remove_reference_t<R>>>> >::value, int> = 0>
constexpr Span(R&& source_range)
: _data(detail::cast_smart_pointer_range_data_to_raw_pointer<T>(source_range))
, _size(std::size(source_range))
{
}
// ...
private:
T* _data = nullptr;
std::size_t _size = 0;
};
【问题讨论】:
-
@JesperJuhl No :)
Span基本上只是一对原始指针和大小。无处可申请std::transform以从vector获得Span。其他向量(以及连续存储其数据的不同类型的容器)通过调用它们的 .data() 成员函数简单地转换为 Span——但在我的特定情况下,如果没有显式转换,它将无法工作。 -
你必须分配
std::vector<MyObject*>或类似的。 -
如果
DoSomething不需要连续内存,那么使用迭代器或范围的更通用接口似乎是合适的。也就是说,如果您可以自己更改方法。我没有使用范围(ranges-v3 和 C++20 Ranges 都没有),但你可以传递你的std::vector<std::unique_ptr<MyObject>>,就好像它是一个带有转换视图范围的原始指针范围,可以选择使用过滤视图。
标签: c++ std unique-ptr c++20 std-span