Heuristic42
Blog
Opengl
Meta
Rendering
3
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
comment
Jan 15 at 7:46
Matrices
[deleted]
–
anonymous
comment
Jan 14 at 8:05
Making a real EMF Reader
All good/my bad. A half implemented feature that I really shoul…
–
pknowles
comment
Jan 14 at 8:03
Making a real EMF Reader
I don't have a circuit diagram sorry. The LEDs are all on separ…
–
pknowles
comment
Jan 10 at 0:07
Making a real EMF Reader
а есть подробные схемы что к чему подключать и куда припаивать…
–
anonymous
comment
Jan 5 at 18:00
Matrices
[deleted]
–
anonymous
comment
Dec 15 '24
Matrices
[deleted]
–
anonymous
comment
Nov 27 '24
DerBard: Custom Split Mechanical Keyboard Prototype
hello
–
anonymous
comment
Nov 20 '24
Matrices
[deleted]
–
anonymous
created
Oct 21 '24
Iterators: pointers vs cursors
You're already doing both of these by hand. This post emphaisze…
–
pknowles
comment
Oct 11 '24
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
*
A few years ago, I was fortunate enough to have Jon Kalb explain move semantics to me. This answered an annoyance I've had with C++ for over a decade. 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 temporary 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 (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 still has the `unique_ptr<>` fluff, but now **the destructor is called once**, just the way we want. Solved, right? Well, 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 actually got really close. `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 make a copy of the object, 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 We've just invented a move-only object. C++11 gave us move semantics to do exactly this, but combined with an 'rvalue' concept to help distinguish between intentional copying and moving. To make it work we have to have a way for the destructor to know whether an object is null or not. Another bool would work, but a value like 'nullptr' can work too. For buffers we can use `0`. Lets update the destructor to check for `0` and add 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 null ("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