我在某些模块的界面中具有以下功能:
void DoSomething(Span<MyObject *const> objects);
,其中Span
是我对C ++ 20的std::span
模板的简化实现。
此函数仅遍历对象的连续指针序列并调用其某些函数,而无需尝试修改指针(因此签名中的const
)。
在主叫方,我有一个std::vector<std::unique_ptr<MyObject>>
。我想将该向量传递给DoSomething
函数,而无需分配额外的内存(用于临时std::vector<MyObject*>
之类的东西)。我只想在恒定时间内将unique_ptr
s的左值向量转换为Span
不变的原始指针。
这必须是可能的,因为带有无状态删除器的std::unique_ptr<T>
的大小和对齐方式与原始T*
指针相同,并且它存储在其中的所有内容只不过是原始指针本身。因此,按字节顺序,std::vector<std::unique_ptr<MyObject>>
必须具有与std::vector<MyObject*>
相同的表示形式-因此必须有可能将其传递给期望Span<MyObject *const>
的函数。
我的问题是:
在当前的std::span
提议中,这样的转换是否可能导致未定义的行为并不会依赖肮脏的hack?
如果不是,那么在以下标准(例如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;
};
答案 0 :(得分:1)
在当前的std :: span建议中,这样的转换是否可能引起未定义的行为且不依赖肮脏的hacks?
不。即使这个陈述是正确的(而且我不知道标准中是否有强制性的要求):
带有无状态删除器的
std::unique_ptr<T>
的大小和对齐方式与原始T*
指针相同,并且存储在其中的所有内容只不过是原始指针本身。
没关系。 unique_ptr<T>
不仅仅是具有某些成员功能的T*
。这是unique_ptr<T>
,由于违反了严格混叠规则,因此试图假装一个是另一个是UB。
如果不是,可以在以下标准(例如C ++ 23)中期望吗?
不。即使P0593的形式以允许将unique_ptr<T>
数组中存储的字节转换为T*
数组的方式进入标准,是 transformation ,而不是演员。也就是说,这些unique_ptr<T>
的生存期将结束,并且T*
数组的生存期将开始使用先前结束的对象中的数据。这样一来,您将无法再使用vector<unique_ptr<T>>
。
任何这样的转换,如果允许的话,肯定会是单向的。 P0593以存储字节为单位隐式创建对象的能力仅限于本质上只是数据字节的类型,而unique_ptr
将不适合该限制。