Как преобразовать std::vector уникальных указателей в std::span необработанных указателей?
У меня в интерфейсе какого-то модуля есть такая функция:
void DoSomething(Span<MyObject *const> objects);
, где Span
это моя упрощенная реализация C++20 std::span
шаблон.
Эта функция просто перебирает непрерывную последовательность указателей на объекты и вызывает некоторые из их функций, не пытаясь изменить указатели (таким образом, const
в подписи).
На стороне звонящего у меня есть std::vector<std::unique_ptr<MyObject>>
. И я хочу передать этот вектор вDoSomething
функция без выделения дополнительной памяти (для чего-то вроде временногоstd::vector<MyObject*>
). Я просто хочу преобразовать вектор lvalueunique_ptr
с к 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;
};
1 ответ
Возможно ли такое приведение с текущим предложением std::span, не вызывая неопределенного поведения и не полагаясь на грязные хаки?
Нет. Даже если это утверждение верно (и я не знаю ни одного требования в стандарте, которое заставляет его быть правдой):
а
std::unique_ptr<T>
с удалителем без сохранения состояния имеет тот же размер и выравнивание, что и необработанныйT*
указатель, и все, что он хранит внутри, есть не что иное, как сам необработанный указатель.
Это не имеет значения. Аunique_ptr<T>
это не просто T*
с привинченными к нему некоторыми функциями-членами. Этоunique_ptr<T>
, и попытка сделать вид, что один является другим, является UB из-за нарушения правила строгого алиасинга.
Если нет, можно ли этого ожидать в следующих стандартах (например, C++23)?
Нет. Даже если форма P0593 попадает в стандарт таким образом, что позволяет хранить байты в массивеunique_ptr<T>
быть преобразованным в массив T*
, это будет преобразование, а не актерский состав. То есть время жизни техunique_ptr<T>
s закончится, и время жизни массива T*
s начнет использовать данные в ранее завершенном объекте. Итак, вы не могли использоватьvector<unique_ptr<T>>
снова после этого.
Любая такая трансформация, если бы она была разрешена, была бы явно односторонней. Способность P0593 неявно создавать объекты в байтах хранилища ограничена типами, которые по сути являются просто байтами данных, иunique_ptr
не укладывается в это ограничение.