2

I'm having trouble understanding what the difference is between using span<T> to create a view over some memory area/buffer as a struct of type T, or using placement-new to initialize a pointer type.

Functionally, they both allow you to write something like foos[0], and to read/write to the underlying memory area.

If I understand it correctly, the placement-new starts the "lifetime" of the pointers in the memory area/buffer -- but functionally, I don't notice any difference in effects between the two.

Below shows an example of what I mean:

//              BUFFER
// |==================================|
// | Page | Page | Page | Page | Page |
// |==================================|
// ^                                  ^
// 0 bytes                            (4096 * 5) bytes

#include <cstddef>
#include <cstdio>
#include <span>
#include <cassert>

struct alignas(4096) Page { int id; };
alignas(4096) std::byte buffer[4096 * 5];

// Using placement-new to initialize Page pointers in the buffer
Page* page_ptrs = new (buffer) Page;

// Using span<T> to create a view of Pages over the buffer
auto pages = std::span<Page>(reinterpret_cast<Page*>(buffer), 5);

int main()
{
    // Get first and second page using page_ptrs
    Page* first_page = &page_ptrs[0];
    Page* second_page = &page_ptrs[1];

    first_page->id = 1;
    second_page->id = 2;

    // Get first and second page using pages
    Page first_page_2 = pages[0];
    Page second_page_2 = pages[1];

    printf("first_page.id = %d, second_page.id = %d \n", first_page->id, second_page->id);
    printf("first_page_2.id = %d, second_page_2.id = %d \n", first_page_2.id, second_page_2.id);

    assert(first_page->id == first_page_2.id);
    assert(second_page->id == second_page_2.id);

    // Update the page ID's using the span<T>
    pages[0].id = 3;
    pages[1].id = 4;
    assert(first_page->id == 3);
    assert(second_page->id == 4);
}
Gavin Ray
  • 595
  • 1
  • 3
  • 10
  • 1
    "*they both allow you to write something like foos[0], and to read/write to the underlying memory area.*" Do they? Because one of them is undefined behavior. It may appear to work, but it's not legal, well-defined C++. There's a lot of stuff in C++ that you can get away with. That doesn't make them legal. – Nicol Bolas Dec 20 '22 at 16:19
  • 2
    `page_ptrs` points to one `Page`, it's also UB to interact with `page_ptrs[1]`. Did you mean to `new (buffer) Page[5]`? – Caleth Dec 20 '22 at 16:24
  • @Caleth/Nicol I edited the example to show what I mean – Gavin Ray Dec 20 '22 at 16:35
  • "Using placement-new to initialize Page pointer**s** in the buffer" is still misleadingly incorrect. There is only one `Page` object starting at `page_ptrs` – Caleth Dec 20 '22 at 17:22
  • Ah, my mistake @Caleth. So, if edited to be Page[5] -- what is the difference between the two since they seem to function identically in terms of being able to read/write to the underlying memory area? – Gavin Ray Dec 20 '22 at 18:44
  • What you started with has undefined behaviour, and what you changed it to doesn't. Undefined behaviour pointedly *doesn't* mean visible breakage – Caleth Dec 20 '22 at 19:47
  • Outside of the well-defined'ness of it, is there any other difference between the "span" and the pointers or are they equivalent? I noticed that in -O the write values are inlined into the print and never stored in memory with span, even if the span is created OVER the placement-new buffer pointers and not reinterpret-casted. – Gavin Ray Dec 20 '22 at 20:08
  • 1
    @GavinRay In general, whatever you do, you need to create the array of the appropriate type. That means you need to `new(buffer) Page[5]`. Whether you then use the pointer returned by the `new` expression or `reinterpret_cast` to it and use it in a `span` is immaterial (although the `reinterpret_cast` technically would require a pass through `std::launder`). In the specific example you are showing, you may be saved by implicit object creation even without the correct `new`, because starting the lifetime of an array of `std::byte` implicitly creates nested objects of implicit-lifetime types. – user17732522 Dec 20 '22 at 20:50
  • Ahh alright, thank you. If you post that as an answer, I'll accept it (not sure if you can since the question is closed I think) – Gavin Ray Dec 20 '22 at 21:50
  • What would you do in the case in which the buffer can be interpreted as one of potentially many kinds of structs? Would you need to initialize the lifetime each time you want to use it as a specific type? – Gavin Ray Dec 20 '22 at 21:51

0 Answers0