Heuristic42
Blog
Opengl
Rendering
Meta
0
comment
Nov 10 at 20:37
DerBard: Custom Split Mechanical Keyboard Prototype
Hi heuristic42.com Owner.
–
anonymous
comment
Nov 10 at 8:40
Embedding GDB pretty printers, just like natvis
Hello 👋. Welcome 🙂
–
pknowles
comment
Nov 10 at 3:44
Embedding GDB pretty printers, just like natvis
Hello heuristic42.com Administrator.
–
anonymous
edited
Oct 13 at 8:08
Why std::move and move semantics?
For years C++ destructors felt like an academic solution that d…
–
pknowles
comment
Oct 13 at 6:39
Clip Space
Thanks for the reminder. Please see [https://www.heuristic42.co…
–
pknowles
comment
Oct 11 at 4:27
Contributing
I have bilateral carpel tunnel and my Microsoft 4k has a torn c…
–
anonymous
edited
Oct 6 at 10:28
Pages About This Site
... in an effort to keep presented content and self discussion …
–
admin
created
Oct 6 at 10:20
Privacy and Data Collection
This wiki stores a small amount of personal data to function an…
–
admin
comment
Sep 12 at 19:24
Clip Space
Hello, We ran a 30-second scan of your domain and found: - …
–
anonymous
created
Jul 7 at 11:21
Hello Ray, a Hello World Vulkan Ray Tracing Tutorial
\*WORK IN PROGRESS\* Want to do some quick 3D graphics progr…
–
pknowles
created
Jul 4 at 6:54
Why std::move and move semantics?
A few years ago, I was fortunate enough to have Jon Kalb explai…
–
pknowles
edited
Jul 4 at 5:15
3D Rendering (Computer Graphics)
Rendering in 3D computer graphics is computing a 2D image from …
–
pknowles
edited
Jul 4 at 4:16
Heuristic42, A Graphics Programming Website
#[Blog](/blog/) An unstructured assortment of personal graph…
–
pknowles
edited
Jun 24 at 15:10
RAII++ - the powerful implication of always initializing
If you search, most definitions of RAII refer to using "**scope…
–
pknowles
created
Jun 24 at 15:01
RAII++ - the powerful implication of always initializing
If you search, most definitions of RAII refer to using "**scope…
–
pknowles
comment
Jun 15 at 11:42
Matrices
[deleted]
–
anonymous
comment
Jun 1 at 11:01
Matrices
[deleted]
–
anonymous
comment
May 18 at 22:30
Matrices
[deleted]
–
anonymous
created
Apr 10 at 12:38
test2
;</script>
–
Nia
comment
Feb 8 at 6:26
Matrices
[deleted]
–
anonymous
edited
Feb 3 at 7:54
Embedding GDB pretty printers, just like natvis
Pretty printers are awesome, but the setup can be a real pain. …
–
pknowles
created
Feb 1 at 12:27
Embedding GDB pretty printers, just like natvis
Pretty printers are awesome, but the setup can be a real pain. …
–
pknowles
comment
Jan 26 at 8:20
Matrices
[deleted]
–
anonymous
…
View All
Log in
Why std::move and move semantics?
leave this field blank to prove your humanity
Article title
*
Article revisions must have a non-empty title
Article body
*
For years C++ destructors felt like an academic solution that didn't fit a lot of real problems. Then I was fortunate enough to have Jon Kalb explain move semantics to me and a decade long annoyance was resolved. Many thanks to friends and coleagues for reviewing and improving this. As a graphics programmer, I started with this: GLuint myBuffer; glGenBuffers(1, &myBuffer); ... glDeleteBuffers(1, &myBuffer); Standard C. But C++ is meant to be object oriented. Can I make a Buffer class? struct Buffer { Buffer() { glGenBuffers(1, &m_buffer); } ~Buffer() { glDeleteBuffers(1, &m_buffer); } GLuint m_buffer = 0; }; Yes! This is great. Now I don't have to remember to call delete, the code is clean and short: struct Mesh { Buffer indices; Buffer positions; }; Mesh loadObj(const char* filename) { ... Buffer indicesBuf = toBuffer(indices); Buffer positionsBuf = toBuffer(positions); return Mesh{indicesBuf, positionsBuf}; } Can you spot the bug? The temporary buffers in loadObj() will have their destructors called at the end of the function. The caller will be left with dangling buffer handles in the Mesh. After seeing this I simply stopped using destructors. I suspect many others did too, before C++11. Even after, we're all still stuck in this mindset that destructors are dangerous and have weird side effects. A seeming workaround is to create the object on the heap. Then you're just moving around a pointer to it and you should only call the destructor once. Mesh* loadObj(const char* filename) { ... Buffer* indicesBuf = toBuffer(indices); Buffer* positionsBuf = toBuffer(positions); return new Mesh{indicesBuf, positionsBuf}; } Mesh* mesh = loadObj("teapot.obj"); ... do something with mesh->indices->m_buffer delete mesh->indices; delete mesh->positions; delete mesh; 😂 don't leave. It's a joke. Of course we'd use `unique_ptr` for this (second joke). To those screaming: bear with me; this is going somewhere. struct Mesh { std::unique_ptr<Buffer> indices; std::unique_ptr<Buffer> positions; }; Mesh loadObj(const char* filename) { ... std::unique_ptr<Buffer> indicesBuf = toBuffer(indices); std::unique_ptr<Buffer> positionsBuf = toBuffer(positions); return Mesh{std::move(indicesBuf), std::move(positionsBuf)}; } Mesh mesh = loadObj("teapot.obj"); ... do something with mesh.indices->m_buffer We can even modify Buffer so that we don't accidentally copy it and get hit by the double-delete bug from before: struct Buffer { ... Buffer(const Buffer&) = delete; Buffer& operator=(const Buffer&) = delete; }; It looks a bit better. It has some `unique_ptr<>` fluff, but now **the destructor is called once**, just the way we want. Solved, right? Not quite. There's a lot more pointer chasing happening now. The CPU will load the memory for `Mesh` but then has to follow the `indices` and `positions` pointers to read the `GLuint m_buffer` from each. Oh, and now we're doing a pile of tiny heap allocations for every `unique_ptr`. This is actually quite bad for performance if we're creating a lot of buffers (ignoring the fact OpenGL buffers themselves are expensive). Clearly C++ destructors just can't be used efficiently. We should all return to calling a `free()` function manually /s. ![don't throw the baby out with the bathwater][1] We're actually getting really close to a great solution. `unique_ptr` is already doing what we need, but very indirectly. I included it because it's so frequently used for this exact case, when people want a move-only object but can't be bothered or haven't experienced how to write one. What we want to do is make sure that there is only one valid `GLuint m_buffer` for a conceptual object at any moment. E.g. if we copy the `GLuint` memory, mark the copied-from object as null so that the destructor won't delete it. We still call the destructor multiple times, but only one call will actually do the deleting. Buffer src = ...; Buffer dst(src); // mark 'src' as null so we only delete one of them Lets first expand what this should do and then factor it. dst.m_buffer = src.m_buffer; src.m_buffer = 0; // now 'src' can know not to call glDeleteBuffers() ... ~Buffer() { if(m_buffer != 0) glDeleteBuffers(1, &m_buffer); } We've just invented a move-only object. C++11 gave us move semantics to help do exactly this, combined with an *"rvalue"* concept to help distinguish between intentional copying and moving. For example, the `Buffer&& other` parameter below indicates `other` will not be used by the caller again. Ownership is being transferred, e.g. it's a temporary, return value or has been explicitly cast with `std::move()`. Buffer(Buffer&& other) { m_buffer = other.m_buffer; other.m_buffer = 0; // mark other as "moved-from" } In general, to make it work we have to have a way for the destructor to know whether an object is valid or not. - One example is `std::optional` which simply has an internal `bool` to know if the object is *"engaged"*. - A special value like 'nullptr' can work too, which is what `std::unique_ptr` uses. For GL buffers we can use `0` because that's not a valid result of `glGenBuffers()`. Lets combine everything: the destructor to check for `0` and a move constructor that sets the `src` buffer to `0`. For completeness we also need a move assignment operator. It's nearly the same, but it needs to handle freeing an existing resource first before moving from the other object. struct Buffer { Buffer() { glGenBuffers(1, &m_buffer); } ~Buffer() { // only delete if not null ("moved-from") if (m_buffer != 0) glDeleteBuffers(1, &m_buffer); // don't set 'm_buffer = 0' - wasteful } Buffer(Buffer&& other) : m_buffer(other.m_buffer) // regular copy { other.m_buffer = 0; // mark other as null ("moved-from") } Buffer& operator=(Buffer&& other) { // beware: moving to self will fail - could check 'this == &other', but costs // free the existing buffer if not moved-from // could factor with the destructor into a private free() method if (m_buffer != 0) glDeleteBuffers(1, &m_buffer); m_buffer = other.m_buffer; // regular copy other.m_buffer = 0; // mark other as null ("moved-from") } // prevent copying // could implement, but it's rare to want to create two identical OpenGL buffers Buffer(const Buffer&) = delete; Buffer& operator=(const Buffer&) = delete; GLuint m_buffer = 0; }; Now what happens if we use this for `Mesh`? struct Mesh { Buffer indices; Buffer positions; }; Mesh loadObj(const char* filename) { ... Buffer indicesBuf = toBuffer(indices); Buffer positionsBuf = toBuffer(positions); return Mesh{std::move(indicesBuf), std::move(positionsBuf)}; } Mesh mesh = loadObj("teapot.obj"); ... do something with mesh.indices.m_buffer The `Mesh` object is now literally just two integers. A mere 8 bytes in memory. No pointer chasing, no heap allocations. The compiler can often remove all the shuffling operations such as copying the int, setting other's to null, checking it before deleting. You get the same code as if you'd done it in C but with less lines and, most importantly, less chance for mistakes. Did you notice that `Mesh` is now back to having just two buffers? It doesn't need a destructor!!! Once your "leaf" objects are copy and move safe, any composed objects do not need any extra code. They just work 🤤. We've just encountered the rules of [3, 5, and zero](https://en.cppreference.com/w/cpp/language/rule_of_three.html): - Initially we had a bug because we added a destructor without implementing (or deleting) the copy constructor and assignment operator --- the rule of 3 - We then added a move constructor and move assignment operator --- the rule of 5 - Now we can use that object without implementing any of them --- the rule of zero ✨ This has been an introduction to RAII. Continuing this convention can have some [powerful consequences](https://www.heuristic42.com/blog/66/raii-the-powerful-implication-of-always-initializing/) for object dependencies, lifetime and modular software design. [1]: https://img.heuristic42.com/img/4ebc6f55f194.jpg
Toggle Preview
Edit message
*
A description of the changes made
Discard Draft
Save Draft
leave this field blank to prove your humanity
Flag
the thing you clicked
for moderator attention.
Reason choice:
Spam, promoting, advertising without disclosure
Rude, inappropriate, generally offensive
Too arrogant or demeaning to others
Other
Reason:
The reason for raising the flag
Error