C++ Weekly - Ep 343 - Digging Into Type Erasure

Поділитися
Вставка
  • Опубліковано 25 вер 2022
  • ☟☟ Awesome T-Shirts! Sponsors! Books! ☟☟
    Upcoming Workshop: Understanding Object Lifetime, C++ On Sea, July 2, 2024
    ► cpponsea.uk/2024/sessions/und...
    Upcoming Workshop: C++ Best Practices, NDC TechTown, Sept 9-10, 2024
    ► ndctechtown.com/workshops/c-b...
    Upcoming Workshop: Applied constexpr: The Power of Compile-Time Resources, C++ Under The Sea, October 10, 2024
    ► cppunderthesea.nl/workshops/
    ► For more information about Visual Assist: www.wholetomato.com/downloads...
    ► For episode notes: github.com/lefticus/cpp_weekl...
    Ep 333 - A Simplified std::function Implementation - • C++ Weekly - Ep 333 - ...
    T-SHIRTS AVAILABLE!
    ► The best C++ T-Shirts anywhere! my-store-d16a2f.creator-sprin...
    WANT MORE JASON?
    ► My Training Classes: emptycrate.com/training.html
    ► Follow me on twitter: / lefticus
    SUPPORT THE CHANNEL
    ► Patreon: / lefticus
    ► Github Sponsors: github.com/sponsors/lefticus
    ► Paypal Donation: www.paypal.com/donate/?hosted...
    GET INVOLVED
    ► Video Idea List: github.com/lefticus/cpp_weekl...
    JASON'S BOOKS
    ► C++23 Best Practices
    Leanpub Ebook: leanpub.com/cpp23_best_practi...
    ► C++ Best Practices
    Amazon Paperback: amzn.to/3wpAU3Z
    Leanpub Ebook: leanpub.com/cppbestpractices
    JASON'S PUZZLE BOOKS
    ► Object Lifetime Puzzlers Book 1
    Amazon Paperback: amzn.to/3g6Ervj
    Leanpub Ebook: leanpub.com/objectlifetimepuz...
    ► Object Lifetime Puzzlers Book 2
    Amazon Paperback: amzn.to/3whdUDU
    Leanpub Ebook: leanpub.com/objectlifetimepuz...
    ► Object Lifetime Puzzlers Book 3
    Leanpub Ebook: leanpub.com/objectlifetimepuz...
    ► Copy and Reference Puzzlers Book 1
    Amazon Paperback: amzn.to/3g7ZVb9
    Leanpub Ebook: leanpub.com/copyandreferencep...
    ► Copy and Reference Puzzlers Book 2
    Amazon Paperback: amzn.to/3X1LOIx
    Leanpub Ebook: leanpub.com/copyandreferencep...
    ► Copy and Reference Puzzlers Book 3
    Leanpub Ebook: leanpub.com/copyandreferencep...
    ► OpCode Puzzlers Book 1
    Amazon Paperback: amzn.to/3KCNJg6
    Leanpub Ebook: leanpub.com/opcodepuzzlers_book1
    RECOMMENDED BOOKS
    ► Bjarne Stroustrup's A Tour of C++ (now with C++20/23!): amzn.to/3X4Wypr
    AWESOME PROJECTS
    ► The C++ Starter Project - Gets you started with Best Practices Quickly - github.com/cpp-best-practices...
    ► C++ Best Practices Forkable Coding Standards - github.com/cpp-best-practices...
    O'Reilly VIDEOS
    ► Inheritance and Polymorphism in C++ - www.oreilly.com/library/view/...
    ► Learning C++ Best Practices - www.oreilly.com/library/view/...
  • Наука та технологія

КОМЕНТАРІ • 51

  • @OskarSigvardsson
    @OskarSigvardsson Рік тому +29

    That animal_view example is really interesting, I hadn't seen that trick with type erasure before. Worth noting also what I find by far the most common usage of type erasure: storing heterogeneous things (but with a common interface) in a collection. For instance, if you want to store a vector of callbacks to run when something happens, you can't use templates since you can't store different types in a single vector. std::vector to the rescue, all the callbacks have been type-erased and can be stored together.

    • @willwu7353
      @willwu7353 Рік тому +3

      The cool thing about lambdas is that they inherently memoize the type used in the body. You can actually use this trick to "polymorphically" store a collection of void*. Because the type is "remembered" in the lambda, when the function ptr is called the right method gets invoked even though they might be different types under the hood. Its a static_cast so no funny dynamic_cast nonsense and RTTI involved. And you only need to store one function ptr per method dispatched. Furthermore if you happen to know the type at runtime you can just set the function pointer on the fly, allowing all kinds of cool behavior like hot-swapping between implementations where you know the type at runtime and ones you dont!

    • @paligamy93
      @paligamy93 Рік тому

      Make a specialization for functions that aren't member functions and you got a deal 🤝 otherwise holding a pointer to something holding a function pointer. So double indirection

  • @bombrix5195
    @bombrix5195 Рік тому +11

    You're kidding, I've spent last 3 days reading about type erasure to implement my own std::function and std::any... Your video about simplified implementation of std::function was very much helpful too!

    • @cppweekly
      @cppweekly  Рік тому +5

      I think if you can fully implement std::function (particularly with small function optimization!) you understand 95% of C++

  • @Pakillototaki
    @Pakillototaki Рік тому +1

    I'm working right now in a kind of "printing" library and I was really overthinking how to make the library agnostic from the real "printable blocks" types. That animal_view example was mindblowing, truly an eye-opener. So simple but so powerful. Thanks for sharing your knowledge

  • @oschonrock
    @oschonrock Рік тому +2

    Very nice! Three more "Likes"!
    I had in fact not seen this "evolution" of the "Sean Parent C++ seasoning" style type erasure without virtual dispatch and ownership machinery inside. Definitely interesting for some cases, as much "thinner".

  • @milasudril
    @milasudril Рік тому +13

    You save one indirection, since you do not have to follow the vtable pointer. Then it got optimized well because you put the pointers in the correct order. As an experiment, try to reverse the data pointer and the function pointer. Then I guess it need to swap rax and rsi to generate a correct function call.
    To protect against dangling references, I prefer std::reference_wrapper. It also fixes the issue of treating a single-argument template constructor as a copy-constructor.

    • @matthieud.1131
      @matthieud.1131 Рік тому

      How would you use an std::reference_wrapper in this context? Wouldn't is prevent you from erasing the type? I assume you would just have the constructor take a std::reference_wrapper instead of a const Speakable& and then still cast it into a const void *, but then how would that protect against dangling references?

    • @milasudril
      @milasudril Рік тому

      @@matthieud.1131 While not foolproof std::reference_wrapper does not accept a T&&

    • @cppweekly
      @cppweekly  Рік тому

      I cover this exact scenario in the notes linked to in the video description. It does definitely change things.

  • @cmdlp4178
    @cmdlp4178 Рік тому +3

    This manual type erasure is useful for implementing stable ABIs or interfacing with C. The class/struct itself is able to be used in C. The template constructor could be a non-class function available in C++ as a safe utility function, while in C you have to implement the type-erased function by hand.

  •  Рік тому +4

    This feels like "duck typing" in Python, however unlike Python here provided animal_view argument having a speak() method is statically ensured. Very cool!
    Maybe this might become a language feature with a simpler syntax for "static duck typing" instead of relying on abstract interfaces. ^^

  • @AniRayn
    @AniRayn Рік тому +4

    12:10 - So a capture-less lambda is essentially an anonymous free function, which you can safely reference throughout the lifetime of the program? This kinda blew my mind, never thought of lambdas that way :)

    • @cppweekly
      @cppweekly  Рік тому

      Yes, once you really take a hold of that you can warp you mind with what is possible for generating new functions and returning / using them in a constexpr context also.

  • @anon_y_mousse
    @anon_y_mousse Рік тому

    343 is a great number. 3 digits with my favorite number in the middle. 7 to the power of 3. A prime to a prime.

  • @kaksisve4012
    @kaksisve4012 Рік тому +5

    Jason: if you do Unreal development
    Me who already develops in C++32: 😱

  • @meowsqueak
    @meowsqueak Рік тому

    How would you suggest extending this to support a mixed collection of such Speakable types, such that item[x].speak() would work for any x up to the size of the collection?

  • @jfmhunter375
    @jfmhunter375 10 місяців тому

    In the animal view example, don't we only have one indirection, calling the function, as opposed to virtual's double indirection (indirecting to the vtable, then indirecting to the right function)?

  • @Boneamps
    @Boneamps Рік тому +1

    This was very nice! Essentially you constructed the vtable yourself by putting the
    void (*speak_impl)(const void*) in there. You could have done the standard type erasure thing with a pure virtual wrapper and an inherited templated class instantiated with the "Speakable" type, but take everything by reference, therefore making the type erased class non owning (or a view as you call it). i:e
    class animal_view
    {
    public:
    template
    animal_view(const Speakable& s) : impl(std::make_unique(s)){}
    void speak(){impl->speak();}
    private:
    struct model{
    ~model(){}
    virtual void speak() = 0;
    }
    template
    struct wrapper : public model
    {
    wrapper(const T& obj) : wrapped(obj){}
    void speak(){ wrapped.speak();}
    const T& wrapped;
    }
    std::unique_ptr impl;
    }
    But in this case, you have overhead of allocating wrapper on the head, and having the compiler generate all the code to construct vtable and fill it appropriately. So by having the function pointers right there in the animal_view, you save yourself the extra indirection of going through the unique_ptr impl. Very cool. I think I might try to use this in practice.
    Would be nice if you could do a few benchmarks and compare this against classical inheritance and runtime polymorphism. My bet is that compiler generated code would be really really similar, and performance pretty much the same.

    • @paligamy93
      @paligamy93 Рік тому

      For other readers: The lack of this feature was acknowledged in the video and is shown in Arthur O'Dwyer's cpp con talk from 2019 and is a great supplemental video to this one

    • @meowsqueak
      @meowsqueak Рік тому

      This one? ua-cam.com/video/tbUCHifyT24/v-deo.html

  • @christiandaley120
    @christiandaley120 11 місяців тому

    Is there a reason to prefer "std::bind_front(&S::operator(), &s)" instead of just passing "s" in directly?

    • @cppweekly
      @cppweekly  11 місяців тому +1

      I might have just over complicated that specific example.

  • @JefersonSantiagodaSilva
    @JefersonSantiagodaSilva Рік тому

    Very cool! The animal view thing could be considered somehow a CRTP-ish implementation?

    • @cppweekly
      @cppweekly  Рік тому +1

      I think that would just confuse the conversation, as CRTP implies that a class inherits from a class the references the derived class.
      class Thing : CrtpThing

  • @fenril6685
    @fenril6685 Рік тому +3

    Great video as always!
    I'm curious as to how different the code generated is between doing type erasure this way versus using templated type erasure methods (with a detection-like idiom) either through C++20 concepts or with SFINAE std::enable_if C++11.
    For instance for those of us stuck working on projects that are still C++11 compliant only, what I would like to know is how much of a difference in performance and/or binary size will we see with an implementation like this (as opposed to what you are doing in the video):
    #include
    #include
    #include
    using namespace std;
    class animal_view
    {
    public:
    template::type* = nullptr>
    explicit animal_view(Speakable const* speakable) : speak_ptr{bind(&Speakable::speak, speakable)}{};

    void speak() const {speak_ptr();}

    private:
    function speak_ptr;
    };
    struct Cow
    {
    void speak() const { cout

    • @cppweekly
      @cppweekly  Рік тому +1

      If I'm reading you correctly, you just mean this method vs using template functions.
      You will pay a slightly higher compile-time cost
      But get better runtimes
      And the size of your binaries will be dependent on the size of the functions you templatize

    • @fenril6685
      @fenril6685 Рік тому +1

      @@cppweekly Yes I meant the lambda version you showed versus something like this which is using a mixture of templates and function object wrappers to achieve a similar effect.
      I think the main difference in the templated version is that you wouldn't be able to hold a vector or map of Speakable objects because Speakable is just a templated typename here.
      I can't really think of a way to solve that sort of thing for compile time, short of implementing a custom static reflection system by customizing a compiler like with LLVM (which I have no experience with sadly) or by creating a custom parser to pregenerate meta files as a pre-build step.
      If you know of a way to get this kind of type erasure effect at compile time only (for obvious performance benefits like you mentioned), please let me know or do a video on it!
      If such a method exists I've yet to learn about it, so this area is probably not covered much.
      Either way, really love the work you do, please keep make your videos!

  • @nmmm2000
    @nmmm2000 Рік тому

    Some time ago, I was obsessed with type erasure like animal_view.
    I really like the way you structured the code. Mine was ugly ;)
    But can you elaborate how is animal_view better from virtual method? I don't think it is.

    • @oligophagy
      @oligophagy Рік тому +7

      The advantage over virtual functions is that there's no base class a Speakable has to inherit from. This could be good for a library because users wouldn't have to modify their whole inheritance hierarchy just to use it. All a Speakable needs is a speak function. You could even go a step further and have animal_view take a pointer to member function as a second (non-type) template parameter so that the speak function wouldn't even need to be named "speak" anymore.

    • @pawello87
      @pawello87 Рік тому

      @@oligophagy And how you store type-erased pointer to member function?

    • @taragnor
      @taragnor Рік тому

      It's sort of the C++ version of duck typing (like in a language like Python or JS), or a trait in Rust, or an interface in Java. Essentially you can use any class, not requiring inheritance or virtual functions or any of that stuff. All you need is a class with a function named "speaks" (or whatever you want), and the view class allows you to call it. It's useful in the sense that you can group objects of multiple types that are linked solely by their ability to do one or more methods of the same name. Granted the C++ version requires a lot more boilerplate than doing it in most other languages, but I mean it's C++, it is what it is.

  • @robertfrysch7985
    @robertfrysch7985 Рік тому +1

    I think this technique is also something that is only possible due to the presents of lambdas and their property to convert to function pointers, because otherwise you don't have the chance to define a (free) function in local function scope. This is interesting since one might think that lambda are only syntactic sugar for classes with call operator.

    • @twitchy9948
      @twitchy9948 Рік тому

      I didn't know about it. How about binding args to the lambda, it will generate a class in that scenario?

    • @robertfrysch7985
      @robertfrysch7985 Рік тому

      @@twitchy9948 In case of a capturing lambda, a conversion to a function pointer is not possible anymore. It only works for non-capturing lambdas.

    • @oligophagy
      @oligophagy Рік тому +2

      You can always just make a private static function template and take the address of the appropriate instantiation in the constructor. This technique has been around a while and definitely predates C++11 lambdas.

    • @twitchy9948
      @twitchy9948 Рік тому

      @@oligophagy would you share an example?

    • @willwu7353
      @willwu7353 Рік тому

      @@twitchy9948 You could also pass the args into the lambda after the void* obj, they are then usable within the lambda and can be passed to the class without needing a capture. If you really want a capture there are ways to do it but all involve storing the lambda and calling its operator() explicitly, i.e not pretty lol.

  • @Runoratsu
    @Runoratsu Рік тому

    I like Visual Assist, but no way to renew a personal license (as in, you have to pay the full price each time, unlike for company licenses) meant I didn’t renew mine last fall. And now that I (have to) use Visual Studio 2022, where my old version won’t install anymore, I’ve switched to using Resharper C++, which my employer pays for as part of R# Ultimate (which we need for C# development)… and I don’t think I’ll ever go back to VA. Another client they’ve lost, unfortunately.

  • @BertLaverman
    @BertLaverman Рік тому +1

    Hmmrph, so any time `static_cast()` turns up the hairs in my neck stand up. But basically, this is like "views" in a language I can't quickly recollect, which asserts the presence of certain methods and/or fields. An excellent example showing C++ could benefit from those. An example of what happens when you try this on a class that doesn't have a `speak()` would have been nice because I am going to need more time to fully comprehend that template. Wanting to squeeze the last out of generated code is fine, but it should not come at the cost of being uncheckable.

    • @sera_kath
      @sera_kath Рік тому +3

      If I understand this correctly: It shouldn't compile if the member function is not present. The call after the cast is actually checked during compile time.

  • @fcolecumberri
    @fcolecumberri Рік тому

    I wonder how much of this could have been done with concepts and templates, I am almoust sure that would have created a larger program since the tamplate would have created a function for each animal, but talking about the runtime it might have been shorter since that would avoid calling a jump (or maybe even more depending on the optimization).

    • @cppweekly
      @cppweekly  Рік тому

      All of it could be done with templates. The point was to play with alternatives to templates.

  • @rafalmichalski4893
    @rafalmichalski4893 Рік тому +1

    Isn't static polymorphism ?

  • @mjKlaim
    @mjKlaim Рік тому

    I thought the tendency is to name these "views" as "any_*", like here "any_animal"? Did the fashion change already? XD

  • @technologicalwaste7612
    @technologicalwaste7612 Рік тому

    Just cast the address to the type of pointer you know matches the layout of the memory.

    • @cppweekly
      @cppweekly  Рік тому +3

      That's UB in C++. They cannot just "happen" to have the same layout