A really brilliant presentation. And Klaus' book "C++ Software Design" is great, by the way. But, after several design patterns, template construction of a concept, with implementations inside hidden friends - how is that KISS? Especially in comparison to the standard and really basic language feature of inheritance? Klaus says "maybe inside it's not that simple, but for the user it's nice" - imagine a user looking at these classes to find out how to extend something (the user has to get to the hidden friends), or just to understand what the interface does. In principle, the user just has to know the design pattern, and preferably everything that goes into it.
The most surprising part of this technique, at least to me, was that the Square doesn't know it's a Shape, or that it can do Shape-ly things, but we do. So that means several people can have a different idea of what a square can do (different ShapeConcept, different number of related free functions) but since all that variation is taken out of the Square, now everyone has the same idea of the "Square itself" so they can send Squares to each other.
43:22 note that std::is_trivially_copyable can (should) be used to verify if copy of the buffer is equivalent to copying the objects. Technically, one could use “if constexpr” in the assignment operator to choose between buffer operation, affordance dispatch or compilation error.Great talk.
External polymorphism was the motivation for the good old Visitor Pattern; but this seems far better, with things able to BE more than one thing without incurring multiple-inheritance.
I appreciate the effort but this is the opposite of KISS. So we replace inheritance with a template class the inherits from a base class to create a wrapper around “any” shape? It may only be me but this is confusing as hell. Bring back inheritance then.
I found this is useful for unify different IO type, e.g. stdio, socket, pipe, named pipe. These IO types have different ways to construct and storage different information. And that information are not important when we use on reading or writing data. All we care on IO types are reading and writing.
Looks like there is a performance issue (time 54:00) related to function "void draw( ShapeConstRef const& shape)". It seems that it would be more efficient code if the function was without const& "void draw( ShapeConstRef shape)" because here we have double-level of pointers dereference. Am I right?
@CppCon at 21:18 wouldn't be more wise to use the strong exception guarantee version of "Shape& operator=(const Shape& s) { return *this = Shape(s); }" ? I don't quite get why I have to swap the pimpl object instead of the actual Shape.. Isn't it possible for the Shape class to have more state, or only pimpl is allowed in that pattern?
got to be honest, that was pretty rough, trying to find silver linings; it was interesting to see the compiler view through the benchmark results he showed, it appeared like basically the compiler just threw out all that extra stuff, like, if it was obvious the compiler cared one way or another than maybe it could have some value as a potential pattern or anti pattern, but no difference from classical inheritance, that's a bit rough
This video takes it slow, so good for novices. Be aware that this is a simple example of Type Erasure, don't think by watching this excellent video you know TE.
I've watched this video twice. I just don't find it convincing. There's a lot of talk of how everything is better theoretically, juxtaposed with code that is so much worse. Definitely the kind of thing I'd only consider after extensive profiling.
I believe MVD got slower in the end because pointer to function construction and call of function pointer has its cost as you create those temporary ShapeRefConst objects.
It’s obviously hard to hypothesize without knowing some details of how Klaus implemented SBO+MVD. However, I assume such an implementation won’t have a non-owning ShapeConstRef (like the basic MVD) because SBO implies ownership. I assume the SBO+MVD would be similar to SBO except the buffer inside the shape will directly have (placement new) the actual specific shape-object (no ShapeConcept/ShapeModel), and apart of that buffer every Shape will have a function-pointer (initialized to a lambda) that correctly static_casts the buffer and calls its free-function. My assumption regarding the perf loss is that perhaps the pure MVD solution had a more compact memory usage (4 vectors of the different types of shapes, and a vector of ShapeConstPtr’s that point into them) while the SBO+MVD was a little less compact because 128 bytes are much larger than the actual space needed. Again, great, thought provoking talk!
@@Roibarkan I'd like to ask you a little offtopic question. What bugs me is creating temporary view objects takes its toll. Imagine matrix library and you want view to row/column and do some operations with it. My tests showed that creation of this temporary object eats too much compared to call some row_fcn, col_fcn. Have you experienced something similar? What did you do to fix this?
Essentially, any struct/class can be a "Shape" as long as it implements the interfaces that Shape requires, and that is amazing, that's what interfaces should just be, without inheritance. This is effectively Go's Interface
I don't like being rude, but what the heck, man. This is basically trying to make up for not having the interface model of Go in C++, but it's almost never worth doing. I _have_ found something like this useful in situations like defining something similar to MetaTypes in the Qt framework (but in my own code) and even that was just to be able to define components of external types that have no knowledge that they are components. Also, boiling types down to void* and size/alignment or whatever internally _is_ in fact type erasure. Anytime you have types in, no types internally, and types out, you can generally call that type erasure.
Did anyone try to use std::shared_ptr to store pimpl object instead on std:unique_ptr? This would remove the need for the clone function inside the concept and any copies will be eliminated! Also the default special member functions will work as expected and there is not need for user defined copy ctor/operator etc. I expect this approach to be more efficient than the simple approach.
If you're copying, the intention behind is that you want to "fork" the lifetime of the original object; with a shared_ptr, you'd be creating two instances of the same class which point to the same "physical" object (in other words, the same memory area).
This is perfectly valid thing to do when all your operations are const - which you also correctly modeled with the const internal type. There are definetly great use cases for that approach as well: my personal example of choice is a generic id type, for which you can even short-circuit the equality operation and skip virtual call if your pointer points to exactly same pointer. I would say this concept of type erasure - although focusing on gaining value semantics - was explored more than 10 years ago in talk "Value Semantics and Concept Based Polymorphism" by Sean Parent (or "Better Code: Runtime Polymorphism" - that one has better quality).
In classic OOP I can do: auto my_circle = make_shared(2.5f); shapes->add_shape(my_circle); // add_shape accepts base class ptr, ex. shape* my_circle->set_radius(1.0f); I can store that pointer and modify this concrete circle any time i want In this type-erasure implementation: auto my_circle = circle{2.5f}; shapes->add_shape(my_circle); // my_circle is moved-out my_circle.set_radius(1.0f); // use-after-move! Well, I'm losing any possibility to modify and even access that circle instance. Conclusion: I prefer to leave lifetime responsibility to the user than give him a fake simplicity by the cost of limited access to his type. I see a lot more advantages in type-erasure based on shared_ptr.
Classic OOP doesn't care about value types and «loves» mutability. Modern OOP cares about value types and tries to avoid mutability, because non-concurrent programming becomes less and less relevant and concurrent mutability is very error-prone and non-scalable.
Not quite. my_circle is copied, not moved. The std::move you see in the constructor is for moving out of the parameter, not the argument passed. The argument will be either copied or moved into the parameter depending on the value category of it, which is determined by how you call the constructor. In your example you passed my_circle which is an lvalue, so it gets copied into the parameter (which in turn is moved into the newly created shape). On the other hand if you wrap your my_circle with std::move, then it will get moved into the parameter (which in turn is moved again into the newly created shape). There is no use-after-move in your example.
This presentation doesn't give C++ a good reputation. Unless you are stuck with using C++14 or earlier, you should just use std::variant if you can provide the list of shape types ahead of time. Just replace std::unique_ptr or the aligned array with std::variant in Shape class, and use a switch statement to dispatch the call to draw/serialize. There is no need for ShapeConcept and ShapeModel. There is no additional allocation from the heap, no placement new, and no virtual function call.
I didn't understand what this talk is about. There were many classes in which it is easy to get confused. It was hard to keep them all in mind. Maybe a class diagram could've helped understanding. I was tied after watching a half of the presentation. Without an example of problem I didn't understand what the problem he was solving... After viewing, there was a feeling of overcomplication
Since C++ introduced alignas it's not necessary to use types that depends on compiler intrinsics (e.g. aligned_storage_t). Both alignas array and aligned_storage_t have equally non-intutive API (user has to explicitely use reinterpret_cast and placement new) so it doesn't really matter. + aligned_storage_t is deprecated since C++23 due to terrible API - ^ + users often incorrectly use aligned_storage type directly instead of aligned_storage::type/aligned_storage_t alias as they supposed to do.
@@maelstrom254 What do you mean "such trivial things as Shape"? Is "an extensible range of types that all share an equally extensible set of operations such as drawing and serialization" just trivial to you? Do you need to create new such hierarchies/sets of types every day in your programming language?
This guy knows a ton about design yet he explains stuff so simply and understandable even novices can a grasp on his topics. Great talk again Klaus!
A really brilliant presentation. And Klaus' book "C++ Software Design" is great, by the way. But, after several design patterns, template construction of a concept, with implementations inside hidden friends - how is that KISS? Especially in comparison to the standard and really basic language feature of inheritance? Klaus says "maybe inside it's not that simple, but for the user it's nice" - imagine a user looking at these classes to find out how to extend something (the user has to get to the hidden friends), or just to understand what the interface does. In principle, the user just has to know the design pattern, and preferably everything that goes into it.
The most surprising part of this technique, at least to me, was that the Square doesn't know it's a Shape, or that it can do Shape-ly things, but we do. So that means several people can have a different idea of what a square can do (different ShapeConcept, different number of related free functions) but since all that variation is taken out of the Square, now everyone has the same idea of the "Square itself" so they can send Squares to each other.
You don't think that's a bad thing? Seems like it would encourage organization balkinization.
seem like duck typing in python
43:22 note that std::is_trivially_copyable can (should) be used to verify if copy of the buffer is equivalent to copying the objects. Technically, one could use “if constexpr” in the assignment operator to choose between buffer operation, affordance dispatch or compilation error.Great talk.
External polymorphism was the motivation for the good old Visitor Pattern; but this seems far better, with things able to BE more than one thing without incurring multiple-inheritance.
I appreciate the effort but this is the opposite of KISS.
So we replace inheritance with a template class the inherits from a base class to create a wrapper around “any” shape?
It may only be me but this is confusing as hell. Bring back inheritance then.
Adding final in the ShapeModel inheritance might help the compiler to better optimize in some cases.
I found this is useful for unify different IO type, e.g. stdio, socket, pipe, named pipe. These IO types have different ways to construct and storage different information. And that information are not important when we use on reading or writing data. All we care on IO types are reading and writing.
Looks like there is a performance issue (time 54:00) related to function "void draw( ShapeConstRef const& shape)". It seems that it would be more efficient code if the function was without const& "void draw( ShapeConstRef shape)" because here we have double-level of pointers dereference. Am I right?
A possible typo report: From slide page 56-68, the term "Concept" should be changed to "ShapeConcept".
I was asking myself the same question: where is this Concept base class coming from?
@CppCon at 21:18 wouldn't be more wise to use the strong exception guarantee version of "Shape& operator=(const Shape& s) { return *this = Shape(s); }" ? I don't quite get why I have to swap the pimpl object instead of the actual Shape.. Isn't it possible for the Shape class to have more state, or only pimpl is allowed in that pattern?
got to be honest, that was pretty rough, trying to find silver linings; it was interesting to see the compiler view through the benchmark results he showed, it appeared like basically the compiler just threw out all that extra stuff, like, if it was obvious the compiler cared one way or another than maybe it could have some value as a potential pattern or anti pattern, but no difference from classical inheritance, that's a bit rough
This video takes it slow, so good for novices. Be aware that this is a simple example of Type Erasure, don't think by watching this excellent video you know TE.
I was waiting for this talk!!!
Hm, Sean Parent did this talk some years ago
At @12:43, should not there be shape->do_draw() instead of shape->draw() inside drawAllShapes()?
Yes. The speaker fixed the naming conflicts in his 2021 CppCon talk here, but he forgot to apply that change to this part.
Excellent talk!
That was fast, good job guys! Great talk!
I've watched this video twice. I just don't find it convincing. There's a lot of talk of how everything is better theoretically, juxtaposed with code that is so much worse. Definitely the kind of thing I'd only consider after extensive profiling.
I believe MVD got slower in the end because pointer to function construction and call of function pointer has its cost as you create those temporary ShapeRefConst objects.
It’s obviously hard to hypothesize without knowing some details of how Klaus implemented SBO+MVD. However, I assume such an implementation won’t have a non-owning ShapeConstRef (like the basic MVD) because SBO implies ownership. I assume the SBO+MVD would be similar to SBO except the buffer inside the shape will directly have (placement new) the actual specific shape-object (no ShapeConcept/ShapeModel), and apart of that buffer every Shape will have a function-pointer (initialized to a lambda) that correctly static_casts the buffer and calls its free-function. My assumption regarding the perf loss is that perhaps the pure MVD solution had a more compact memory usage (4 vectors of the different types of shapes, and a vector of ShapeConstPtr’s that point into them) while the SBO+MVD was a little less compact because 128 bytes are much larger than the actual space needed. Again, great, thought provoking talk!
@@Roibarkan I'd like to ask you a little offtopic question. What bugs me is creating temporary view objects takes its toll.
Imagine matrix library and you want view to row/column and do some operations with it. My tests showed that creation of this temporary object eats too much compared to call some row_fcn, col_fcn.
Have you experienced something similar? What did you do to fix this?
Great!
Are the 2500 translates() per shape object, or per shape type?
is there source code available for this talk?
Essentially, any struct/class can be a "Shape" as long as it implements the interfaces that Shape requires, and that is amazing, that's what interfaces should just be, without inheritance. This is effectively Go's Interface
I don't like being rude, but what the heck, man. This is basically trying to make up for not having the interface model of Go in C++, but it's almost never worth doing. I _have_ found something like this useful in situations like defining something similar to MetaTypes in the Qt framework (but in my own code) and even that was just to be able to define components of external types that have no knowledge that they are components. Also, boiling types down to void* and size/alignment or whatever internally _is_ in fact type erasure. Anytime you have types in, no types internally, and types out, you can generally call that type erasure.
Did anyone try to use std::shared_ptr to store pimpl object instead on std:unique_ptr? This would remove the need for the clone function inside the concept and any copies will be eliminated! Also the default special member functions will work as expected and there is not need for user defined copy ctor/operator etc. I expect this approach to be more efficient than the simple approach.
But the thing is that we want to have a copy! In your case we just have copy a pointer to ShapeModel but not copy of ShapeModel itself
If you're copying, the intention behind is that you want to "fork" the lifetime of the original object; with a shared_ptr, you'd be creating two instances of the same class which point to the same "physical" object (in other words, the same memory area).
This is perfectly valid thing to do when all your operations are const - which you also correctly modeled with the const internal type. There are definetly great use cases for that approach as well: my personal example of choice is a generic id type, for which you can even short-circuit the equality operation and skip virtual call if your pointer points to exactly same pointer.
I would say this concept of type erasure - although focusing on gaining value semantics - was explored more than 10 years ago in talk "Value Semantics and Concept Based Polymorphism" by Sean Parent (or "Better Code: Runtime Polymorphism" - that one has better quality).
@@GrzesiuG44 Great talks thank you for pointing them out to me. I was just 10 years late!
In classic OOP I can do:
auto my_circle = make_shared(2.5f);
shapes->add_shape(my_circle); // add_shape accepts base class ptr, ex. shape*
my_circle->set_radius(1.0f);
I can store that pointer and modify this concrete circle any time i want
In this type-erasure implementation:
auto my_circle = circle{2.5f};
shapes->add_shape(my_circle); // my_circle is moved-out
my_circle.set_radius(1.0f); // use-after-move!
Well, I'm losing any possibility to modify and even access that circle instance.
Conclusion: I prefer to leave lifetime responsibility to the user than give him a fake simplicity by the cost of limited access to his type.
I see a lot more advantages in type-erasure based on shared_ptr.
Classic OOP doesn't care about value types and «loves» mutability. Modern OOP cares about value types and tries to avoid mutability, because non-concurrent programming becomes less and less relevant and concurrent mutability is very error-prone and non-scalable.
@@blacklion79 Ok, but you lose access to *any*, even non-mutating method.
Not quite. my_circle is copied, not moved. The std::move you see in the constructor is for moving out of the parameter, not the argument passed. The argument will be either copied or moved into the parameter depending on the value category of it, which is determined by how you call the constructor. In your example you passed my_circle which is an lvalue, so it gets copied into the parameter (which in turn is moved into the newly created shape). On the other hand if you wrap your my_circle with std::move, then it will get moved into the parameter (which in turn is moved again into the newly created shape). There is no use-after-move in your example.
This presentation doesn't give C++ a good reputation. Unless you are stuck with using C++14 or earlier, you should just use std::variant if you can provide the list of shape types ahead of time. Just replace std::unique_ptr or the aligned array with std::variant in Shape class, and use a switch statement to dispatch the call to draw/serialize. There is no need for ShapeConcept and ShapeModel. There is no additional allocation from the heap, no placement new, and no virtual function call.
He addressed this exact point in the Q&A. They do different things.
Nice talk, but the loud sibilance made it painful to listen to. Need better microphone to tone down those super-loud 'ssssssss'.
C++ is dead to me, if such trivial things require so much effort 😢
I didn't understand what this talk is about. There were many classes in which it is easy to get confused. It was hard to keep them all in mind. Maybe a class diagram could've helped understanding. I was tied after watching a half of the presentation. Without an example of problem I didn't understand what the problem he was solving... After viewing, there was a feeling of overcomplication
The first part of this talk describes the problem with the traditional approach using inheritance:
ruclips.net/video/4eeESJQk-mw/видео.html
Actually, C++ including templates does not need all this C++11/C++17 etc. stuff.
The consultants need it.
C++ is going to be destroyed.
Inheritance is a bad design pattern. This talk is about getting dynamic polymorphism WITHOUT inheritance exposed in the interface.
Skill issue
37:20 I believe that std::aligned_storage_t would be better suited for this rather than std::array
aligned_storage is deprecated in C++ 23
Since C++ introduced alignas it's not necessary to use types that depends on compiler intrinsics (e.g. aligned_storage_t).
Both alignas array and aligned_storage_t have equally non-intutive API (user has to explicitely use reinterpret_cast and placement new) so it doesn't really matter.
+ aligned_storage_t is deprecated since C++23 due to terrible API - ^ + users often incorrectly use aligned_storage type directly instead of aligned_storage::type/aligned_storage_t alias as they supposed to do.
I believe type-erased C++ is called C...
Very far from it. C should be labeled deprecated.
@@chrisminnoy3637 rather C++ is deprecated, if such trivial things as Shape require such insane amount of effort to implement 🤯
@@maelstrom254 sure, whatever you say bro, if you want to stick to 8 bit processors and only simple logic to run.
@@maelstrom254 What do you mean "such trivial things as Shape"? Is "an extensible range of types that all share an equally extensible set of operations such as drawing and serialization" just trivial to you? Do you need to create new such hierarchies/sets of types every day in your programming language?
Generally nice design, but that first optimization is nasty. Manually calling a destructor? No way