How to use FUNCTION TEMPLATES - a comprehensive guide for modern C++

Поделиться
HTML-код
  • Опубликовано: 13 сен 2024

Комментарии • 33

  • @CodeForYourself
    @CodeForYourself  6 месяцев назад +3

    Hey everybody. This video took _a while_ to plan and record. Please comment if anything is unclear or, on the contrary, if you liked some explanation. Hope there won't be too many mistakes. 😅
    Also, if you like what I do - smash that "Super Thanks" button 😉

  • @dino_source
    @dino_source 6 месяцев назад +4

    I am a simple man. I see a new video from `Code for yourself`, I hit the thumb up button.

  • @yurkoflisk
    @yurkoflisk 6 месяцев назад +3

    19:21 The explicit specialization is called in this case.
    As you mentioned, overload resolution, irrespective of where (and whether) the explicit specialization is declared, chooses `void Process(T*)` as a better match when calling `Process(pointer)`. Now, when the explicit specialization is declared *above* `void Process(T*)` it specializes `void Process(T)`, so the chosen template `void Process(T*)` doesn't have any explicit specializations and hence is (implicitly) instantiated from the template itself to form the (instantiated) specialization `void Process(int*)`.
    When the explicit specialization is declared *below*, though, both templates are seen from it and both can deduce their T from the specialization, thus the ambiguity of which of the templates is actually being specialized is, per [temp.deduct.decl#2], resolved by partial ordering defined in [temp.func.order] and [temp.deduct.partial], i.e., a _more-specialized_ function template is preferred. In our case, `void Process(T*)` is more specialized than `void Process(T)` because `T` in `void Process(T)` can be deduced from `U1*` in `void Process(U1*)` while `T` in `void Process(T*)` can't be deduced from `U2` in `void Process(U2)`, where U1 and U2 are some unique synthesized types. Thus, this time it is `void Process(T*)` that is being specialized by our explicit specialization (resulting in specialization `void Process(int*)` being explicit, not instantiated), so when `void Process(T*)` is chosen by overload resolution, its specialization is defined as we intended.

    • @CodeForYourself
      @CodeForYourself  6 месяцев назад +1

      Yep, you’re totally right! Thanks for answering 🙌

  • @Regis-0
    @Regis-0 6 месяцев назад +1

    Why can't I subscribe second or third time 😅 Great video!

    • @CodeForYourself
      @CodeForYourself  6 месяцев назад

      Thanks! 🙏 That would’ve been great 😊

  • @Awesomekid2283
    @Awesomekid2283 5 месяцев назад +1

    Really great video and I love how much you went through in such a short amount of time. I was messing around with code to produce the two problems with template specializations and I found some interesting behavior I was wondering if you could shed some light on. For the record, I was using C++23 and Clang. The code I tested first for the problem that template's will never match reference types unless they are explicitely specified worked exactly the way you showed. However, I tried the Dimov/Abrahams problem using an Object pointer instead of an int pointer becuase I had created an object class for testing the previous example. Everything worked like I observed in your video until I tried changing the specialization to a function overload. My code still picked the template-function overload instead of the function overload! This was really confusing to me since I thought regular functions were always preferred over templates. I even tried moving the functions around, but no order I put them in changed the result.
    This stumpted me to no end, I tried the same example with an int and it worked just like you said it would! It seems the rules for templates are different for object pointers for some reason? I then thought that maybe the fact that I had these functions in a header file I was including was contributing to the problem because of course they were marked inline when necessary, so I tried in my main file and I got exactly the same behavior. This actually only ended up confusing me more because I encountered more strange behavior I wasn't expecting regarding the namespace from my header file. As usual, I put the namespace name in front of all my function calls since I didn't declare that I was using that namespace. CLion said that the namespace name wasn't necessary on the function calls where I specifcally passed an object or the address of an object, but it was necessary on my function calls where I was passing in an int or the address of an int. I then tried running my code and lo and behold I got errors when removing the namespace on my primitive function calls but I didn't when I did it for my object calls! I have absolutely no idea what was going on here and I would love an answer. It got weirder yet again because I had functions with the same name in both my main file and my header file, but I didn't get an error when omiting the namespace specifically on the calls where I was passing an object or the address of an object, which was strange because I thought there would have been a name conflict, but even worse, C++ actually picked the function from the header file in a different namespace instead of the one in the current namespace! I've seen some bizarre things in C++, but this is one of the most bizarre and I'm completely lost. I had to use the global-namespace specifier to get C++ to choose the function from it's current namespace which didn't make any sense.
    Even weirder still, this was only a problem when I called the function with an object. When I called the function with the object's address, the correct one was picked!
    I know you're probably busy, but I would love it if you could take the time to answer or direct me to a video explaining it if you know of one!
    Here is the link: godbolt.org/z/sdGsWra8G.

    • @CodeForYourself
      @CodeForYourself  5 месяцев назад +1

      Sounds interesting. I’d like to give it a go. Can I ask you to setup a small reproducer on compiler explorer and share the link here?

    • @Awesomekid2283
      @Awesomekid2283 5 месяцев назад +1

      @@CodeForYourself I just realized today that RUclips deleted my reply because I guess it thought the link was spam. I'll try to do my best to get it to go through. I can't give you the full link, but if you go to the normal url for Compiler Explorer, it's a '/z/' then sdGsWra8G. Let me know if that works! Thank you for taking the time!

    • @Awesomekid2283
      @Awesomekid2283 5 месяцев назад

      @@CodeForYourself I'm sorry it took me so long to respond! I thought I had, but it seems RUclips won't let me reply with the link. It actually kept deleting my reply! I just edited my original comment to include the link at the bottom. Thank you, again, for taking the time!

    • @CodeForYourself
      @CodeForYourself  5 месяцев назад

      @@Awesomekid2283 thanks a lot. Got it. Will have a look soon!

    • @CodeForYourself
      @CodeForYourself  5 месяцев назад

      @@Awesomekid2283 Ok, I've looked into your example and see a couple of issues that could cause the behavior you describe.
      First of all, your functions are all `inline` in the header. Which means that they already have all of their definitions. You don't need to (and generally cannot, but see the next issue point) redefine such `inline` functions in the cpp file. You are only able to do it because of the namespaces that you use. Which brings us to the second point.
      Second, you have different namespaces in the header file and the source file. All of the functions in the header file are in the `templates` namespace, while all of the functions in the cpp file are in the global namespace. Now what happens is this: the functions in the header file are fully defined. The functions in the cpp file *do not define previously declared functions, they define new functions in the global namespace*. This causes all of the issues that you experience. Does this make sense? If you try to add the namespace `templates` in your cpp file, you will get quite a few "redefinition" errors during the compilation.
      I would generally recommend to try to simplify your testing setups. I do it myself. If I want to make sure that I understand a certain concept, like template specialization, I build up a *minimal* example - as small as I can, to make sure I'm only testing what I wanted to. In your case, you have at least three things going on in the same time - header/source file separation, namespaces and then template specialization/overloading. Separating these into separate small examples lets us learn them more easily.

  • @zamf
    @zamf 6 месяцев назад

    As usual nice and concise explanation of a complex C++ topic. Templates are possibly the language feature with the most time spent on teaching them. I don't know of any other topic that so useful and at the same time so full of corner cases, exceptions and nuances than templates and template metaprogramming.
    One thing that is interesting to mention is the ability to "disable" a template specialization when substitution for the template arguments is not possible, also known as SFINAE. Although, it is mostly being replaced by concepts since C++20. Maybe in a future video you could mention about concepts and how they affect the overloading rules when choosing the correct template specialization.

    • @CodeForYourself
      @CodeForYourself  6 месяцев назад +2

      Yeah. You’re totally right! I’ve been thinking what to put into this video and decided to not go too deep straight away and have a separate video that will probably combine SFINAE with concepts 🤔 But I’m still thinking about it.

    • @bsdooby
      @bsdooby 6 месяцев назад +1

      I recommend your channel/videos to my C++ students

    • @bsdooby
      @bsdooby 6 месяцев назад

      oh, a typo "multiple tempate parameters"...

    • @CodeForYourself
      @CodeForYourself  6 месяцев назад

      @@bsdooby oh-oh. In the video? Where?

    • @bsdooby
      @bsdooby 6 месяцев назад

      @@CodeForYourself Yes; one of the "blue chapter screens" @ approx. 4.45

  • @ytacctaccnt
    @ytacctaccnt 6 месяцев назад +1

    Pretty good explained. Considering other videos now.. (Not robot btw :) )

    • @CodeForYourself
      @CodeForYourself  6 месяцев назад +1

      Thanks for giving it a go! Hope you like the other videos too! My favorite is on move semantics: ruclips.net/video/kqQ90R0_GFI/видео.htmlsi=wOisw4cmw4bFPg4C
      But people seem to love the google testing one most: ruclips.net/video/pxJoVRfpRPE/видео.htmlsi=LCAPL2nSCyROrP-X

  • @bsdooby
    @bsdooby 6 месяцев назад +2

    of course is correct 😅

  • @GoWithAndy-cp8tz
    @GoWithAndy-cp8tz 6 месяцев назад

    I wonder why statically typed language like C++ is so messed up with unnecessary features just because to look like dynamically typed ... What is the gain of templates because not readability and clearness for sure? Cheers!

    • @CodeForYourself
      @CodeForYourself  6 месяцев назад +3

      That’s a good question. There are multiple reasons why C++ is so complicated. The main one is that it is a language that has been in active use for around half a century. Just let that achievement sink in. At the same time it manages to be quite modern as languages go today. Now to make it be modern now while supporting all the stuff that is used all over the world it inevitably becomes quite complex.
      As for the templates. They allow us to not pay the runtime cost for our abstractions. Which is really handy. With dynamically typed languages we pay a lot of runtime cost for their operation. Furthermore there are some context (think automotive, nuclear, military etc) that have to be fully deterministic including the memory allocation. That’s when C++ shines. It allows us to write abstract code while satisfying those requirements.
      And as for complexity, yes all the details are complex, but the ideas are not that complicated. In the end if you think about it, the details are complicated in any language.

    • @zamf
      @zamf 6 месяцев назад +4

      The truth is that statically-typed languages try to provide ways of declaring loosely-typed constructs, like templates, variants, type-erasure, any-type and so on, while dynamically-typed languages try to provide ways of declaring strongly-typed constructs, like for example with Typescript vs. Javascript.
      Each of the two approaches has its benefits and drawbacks.
      The main benefit of statically-typed languages in my opinion is that classes and types in general come with a lot of predetermined safety checks done by the compiler. If I declare a variable `int` I know it will hold a number inside and this is guaranteed by the language. I don't have to do any checks and I can focus on other stuff. With dynamically-typed languages you never have these compiler guarantees that a certain variable holds a certain type of value. So every time you use a variable you need to be sure that you're not working with some incompatible data.
      On the other hand dynamically-typed languages let you focus on the problem you're solving and not on some type-safety acrobatics you have to do like with statically-typed languages.
      C++ templates take the best of both worlds because you still get all the compiler checks and guarantees but you can also tell the compiler "hey, compiler, this `typename T` is a type, don't worry about it, assume it's just any type and move on". So you can still use whatever type you want. Even types that other people have defined and that you've never even thought of. As long as these types comply with your template.
      Edit: A good example of the type-safety guarantees of statically-typed languages is Rust's NonZero integers, like NonZeroU32. This type guarantees at compile-time (i.e. while writing the code) that the value inside will not be 0. So, if you decide to divide by this number you're 100% guaranteed that the result will be some value and not NaN. With Javascript, for example, a NaN is always a possibility when doing divisions and there's no solution rather than asking "Is this value 0? If no, then do the division. Otherwise, don't divide but return some other value instead". That is some noise in the code you don't wanna have and Rust gives you a way to avoid it.

    • @CodeForYourself
      @CodeForYourself  6 месяцев назад

      @@zamf a very good answer! Fully agree!👍