I have the following function in the interface of some module:
void DoSomething(Span<MyObject *const> objects);
, where Span
is my simplified implementation of the C++20's std::span
template.
This function just iterates over a contiguous sequence of pointers to objects and calls some of their functions, without attempting to modify the pointers (thus the const
in the signature).
On the caller's side, I have a std::vector<std::unique_ptr<MyObject>>
. And I want to pass that vector to the DoSomething
function without allocating additional memory (for anything like a temporary std::vector<MyObject*>
). I just want to convert an lvalue vector of unique_ptr
s to a Span
of immutable raw pointers in constant time.
It must be possible, because a std::unique_ptr<T>
with a stateless deleter has the same size and alignment as a raw T*
pointer, and all it stores inside is nothing but that raw pointer itself. So, bytewise, std::vector<std::unique_ptr<MyObject>>
must have the same representation as std::vector<MyObject*>
-- thus it must be possible to pass it to a function which expects a Span<MyObject *const>
.
My question is:
Is such a cast possible with the current proposal of
std::span
without causing undefined behavior and relying on dirty hacks?If it's not, could it be expected in the following standards (e.g., C++23)?
What are the dangers of using a cast that I implemented in my version of
Span
, using a dirty trick withmemcpy
? It seems to work fine in practice, but I suppose there might be some undefined behavior in it. If there is, in which cases can that undefined behavior shoot me in the foot on MSVC, GCC or Clang/LLVM, and how exactly? I would be grateful for some real examples of such scenarios, if they are possible.
My code goes like this:
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;
};