Semelhante ao exemplo da std::aligned_storage
página aqui , imagine que eu tenho o seguinte:
template <typename T, std::size_t N>
class SlotArray {
static_assert(!std::is_array_v<T>);
static_assert(!std::is_void_v<T>);
using StorageType = std::aligned_storage_t<sizeof(T), alignof(T)>;
public:
template<typename ...Args>
T* alloc(Args... args) {
std::size_t index = claimIndex();
return ::new(&slots[index]) T(std::forward<Args>(args)...);
}
void free(T* value) {
static_assert(sizeof(std::ptrdiff_t) <= sizeof(std::size_t));
StorageType* storagePtr = reinterpret_cast<StorageType*>(value);
free(storagePtr - slots);
}
void free(std::size_t index) {
std::destroy_at(std::launder(reinterpret_cast<T*>(&slots[index])));
freeIndex(index);
}
private:
std::size_t claimIndex() {
// Get a unique index, doesn't matter here how (say, bitset + throw if empty)
...
}
void freeIndex(std::size_t index) {
...
}
class alignas(T) Storage {
std::byte buffer[sizeof(T)];
};
StorageType slots[N];
};
Tem algum UB aqui?
A alloc
função membro é o posicionamento-novo padrão e a free
sobrecarga da função membro com um parâmetro de índice é std::launder
coisa de livro didático. A aritmética de ponteiro é bem definida. A parte que é mais difícil de verificar é o reinterpret_cast
tipo de armazenamento subjacente. Fora de alguns casos extremos com matrizes, há muitas indicações de que o tipo de ponteiro do posicionamento novo deve ser o mesmo endereço. Além disso, o resultado da conversão claramente tem o mesmo alinhamento e, na verdade, aponta para algo na matriz.
Certamente, tudo isso pressupõe que o usuário não passe nenhum ponteiro aleatório T*
, mas apenas ponteiros retornados de alloc
.
Conforme apontado em um comentário por @AhmedAEK,
std::aligned_storage
está obsoleto e possivelmente causa UB (veja P1413R3 ). Além disso, o comentário continha um link para a apresentação "An (In-)Complete Guide To C++ Object Lifetimes" de Jonathan Müller, que me levou a uma versão mais longa aqui . Foi muito útil para entender as partes relevantes do padrão que se aplicam a esta questão.Primeiro, vamos nos livrar do uso de
std::aligned_storage
. Para a substituição, P1413R3 sugere usar um array de bytes alinhado corretamente. Esse é um comportamento explicitamente definido, cujos detalhes são abordados em [intro.object] (ele até tem um exemplo de uso de placement-new da maneira exata que se segue).Com isso,
alloc
fica:O
void*
gesso é usado para garantir que quaisquer sobrecargas de posicionamento sejam evitadas.Em seguida, a
free
sobrecarga com osize_t
parâmetro se torna:Note que o uso de
std::launder
é necessário, pelo menos a partir do C++23. Para detalhes, veja P3006R0 .Finalmente, chegamos ao cerne da questão. A resposta está na noção de interconvertibilidade de ponteiros, cujos detalhes são abordados em [basic.compound] . Um ponteiro para o objeto e o ponteiro para o armazenamento não são (ainda) interconvertíveis por ponteiros, mas podem ser convertidos com segurança de um para o outro, conforme indicado em P3006R0. No entanto, um ponteiro para o
storage
membro de aSlot
é interconvertível por ponteiros com um ponteiro para o recipienteSlot
porque "um é um objeto de classe de layout padrão e o outro é o primeiro membro de dados não estático desse objeto".