As long as one is clear that a shared pointer dot something is a method/property of the shared pointer and arrow is an overloaded operator for accessing stuff of the instance pointed to we should be fine I guess.
This is not a problem, as long as one has the basics of the syntax down. If this is a problem, then: p = 0; vs. *p = 0; …would be even more problematic. But here too, there is no problem whatsoever. Both can be perfectly valid statements, to be used at one’s discretion.
A huge pet peeve I have is when IDEs will automatically replace `.` with `->` for types that overload it like smart pointers and optional because they want to be "helpful" and it actually makes my life significantly harder. If I wanted an arrow, I'd type an arrow. It's one more character and not that much effort. Who's saving significant time and effort by not thinking about when to use dot vs arrow?
This is a nasty one. I once spent 2 hours debugging a similar situation, although my unique_ptr was pointing to a custom object that had reset(). After this incident I have started to name my reset functions differently.
This reminds me of the old debate about whether std::optional should be able to contain reference types (it can't). If allowed, it becomes ambiguous whether the assignment operator assigns to the std::optional itself or the referred-to object. (See Stack Overflow question "Why does std::optional not have a specialization for reference types?")
Except that one was solved long before anyone even suggested the possibility that "assign through" was a valid implementation technique. Rebinding is the only feasible strategy for implementing `std::optional`. See: "To Bind and Loose a Reference" for more details on that. Thankfully, P2988 - "std::optional" is on it's way to being accepted, putting an end to this phantom counter argument against the only version of `optional` that has ever been implemented.
if resetting the underlying object, I would set an auto variable to the pointer to the variable by calling std::unique_ptr::get() and then dereference that with a call to reset(). This makes it clear to a reader what the intention is, and the compiler will optimize the code to the second call in the example.
It reminded me somewhat similar problem with passing `std::shared_ptr` as function argument as const ref or const value. Passing it like `void foo(const std::shared_ptr& val)` gives wrong impression that the object stored by this `shared_ptr` is constant. In fact the pointer is constant but pointing to thing can be modified.
well, never been bitten by confusion on reset() per smart pointers at least - always have keen awareness of when accessing the smart pointer vs the pointer it wraps because C++ has ver distinct syntax for dereferencing a pointer. And there has often been discussion that maybe there should just be '.' operator for dereferencing to a field or method, but in this case it's helpful to have a distinct operator for dereferencing pointers to fields or methods.
Interestingly Rust has a similar ambiguity, though you don't even have the -> operator to tip you off! Luckily, there is an idiom to make a method that may collide in this way be only callable using fully qualified syntax, i.e. Type::method(receiver) as opposed to receiver.method.
This tactic is called an "associated function" as distinct from a method, or rather, a Rust method is just an associated function in which our first parameter is some variant of the keyword self, and so the compiler can see how to perform the argument re-arrangement to transform a method call into the actual associated function call. C++ has never managed to agree how such a thing should work in C++, it's part of the discussion about UFCS (Uniform Function Call Syntax).
I dont think it's a problem or not-obvious with that specific example, because std::any does not expose a -> operator. Now try with a std::unique_ptr , with T exposing one of the same functions, that's the ambiguous-at-reading case for me (although it's clear to the compiler).
When preparing for this episode I originally used unique_ptr but had someone complain that was an unrealistic example, so I just tried to come up with something else :)
I suppose static analysis could indeed be used to ban this pattern, and require a NOLINT that forces people to think twice, as well as documenting for the readers. That's exactly what the clang-tidy check about easily swappable parameters does :)
you cant really, since even in this case, having reset methods both in the wrapper class and in the wrapped value class is totally coincidental. Unless you gonna ban every usage of that wrapper\wrapped value pair, that can be potentially problematic. Feels like "there's a nasty spider, let's burn the whole house" kind of solution
@@Raspredval1337 You can programatically check that the wrapped/wrapper class both have the reset() method, and issue a warning if the method is called. No need to hardcode a list of possible problematic pairs (if that's what you meant). Sure, such a warning does not mean the code is wrong. One could see them as False Positives and perhaps the check wouldn't be very useful. But that would not be the point of the check. Instead, it would be like a STOP sign when driving - you may drive, but you need to stop, add a NOLINT, and document the intention of the code. This may be overkill for some people/projects, or valuable for others.
I kind of remembering touching this on Twitter/X. Wondering if it was me. Anyway... I am reading good comments about using Clang Tidy to ban this sort of behavior in scenarios where Container and type T, both, contain reset methods (user-defined or not). Banning/Flagging all these cases would leave to many false positives or something like but, what if: you check the following lines? If you do reset on the container (e.g. unique_ptr) and then you keep using the container as if nothing happens, that can be checked, right? The other way around seems more difficult (IMO), because you can reset the underlying type and still have defined behavior.
This is an example of the tradeoffs of collapsing levels of indirection. Such problems aren’t even limited to programming. I’ve seen similar apparent ambiguity in legal documents and manufacturing bills of materials, for example. In each of the cases, the onus has to be on the reader to inform themselves of how to correctly interpret the reference. Putting the onus on the author to write such things that are self-evidently unambiguous to uninformed readers means writing in a way that is unreadably cluttered with details.
I like Rust's approach (which they unfortunately didn't take from the very beginning) where operations on smart pointers aren't methods but associated functions. So instead of using `let x = Rc::new(3); let y = x.clone();` where `x.clone()` is ambiguous, you'd call `let y = Rc::clone(&x);`
i'd try not to use the method names from unique_ptr in my own types because I know my types are likely to be used in a unique_ptr eventually. I also have the opinion that operator overloading and conversion operators are usually not as good an idea as they initially seem to be and should just be avoided. and this is another example of that. i.e. overloading operator-> on your own type makes it confusing to use with pointers and you're probably going to use it with pointers eventually. Honestly though, I don't like the popular internet answer "skill issue", but I think it applies here. Pointers to things can be nested and you should know what level of pointer or thing you're working at, because nested pointers will always have the same pointer methods and operators
But it doesn't (always) free the resources. The other use-case is to assign a new object, that's why 'reset'. It's just the short form of 'x.reset(nullptr);'
An argument could be made that the two methods should have different names. The problem is that "reset" is too generic. We could choose a different name for each method but we would probably run out of names and it would make the language harder to learn. The natural choice for the method name is to make it reflect what it actually does on a per-class basis. On the other hand, we don't want to unnecessarily expose implementation details even in method names. I think we are stuck with this problem. Better to solve it with a compiler that detects the potential problem and warns the programmer. AFAIK, that would be a level higher than what compilers do these days.
I think the problem is all of the std classes that overload operator->. Renaming reset wouldn't do anything, because any name can be used in a user defined class, so maybe the only solution is a linter rule which forbids calling operator-> on unique_ptr/any/optional/etc. to make sure this doesn't occur.
@@TsvetanDimitrov1976 I view the "reset" issue here as just an example of a much bigger problem. Though, as I think you are trying to point out, there would be no problem if the pointer wasn't a smart pointer and, therefore, didn't have methods like "reset". Still, I don't think we should abandon smart pointers in order to solve a problem like this one. After all, it is only a minor inconvenience that occasionally a programmer will get it wrong and have to debug it. Perhaps we should just consider it a form of job security. LOL
I would argue for either forcing a reset of the pointer to necessitate calling the function with an argument, i.e. no default arguments, make it explicit; or alternatively renaming the function as it doesn't make as much sense with that name as it could with another. I would also suggest that maybe a simple wrapper class shouldn't use the same idioms as a full-blown container class, but maybe that's just me thinking that.
its the problem of the language if its easily ambiguous and complex. use an easier language if its so complex that you have to teach how to use it. ie k.i.s.s. it will go wrong if it can go wrong. yep having the machine-assembly language-like pointer variables and non-pointer variables just makes things more complex. for no reason. just use references like a sane usable level language would do. yep c/c++ is a mid-level language, not actual high-level language. the more you have to fiddle with low level features like memory and pointers, the lower the level of the language. not even if its ~generally high level. the more maintenance and thinking you have to do on the language itself, the lower level it is, you are no longer programming the app, you are programming about the language itself. focus matters.
As long as one is clear that a shared pointer dot something is a method/property of the shared pointer and arrow is an overloaded operator for accessing stuff of the instance pointed to we should be fine I guess.
This is not a problem, as long as one has the basics of the syntax down. If this is a problem, then:
p = 0;
vs.
*p = 0;
…would be even more problematic. But here too, there is no problem whatsoever. Both can be perfectly valid statements, to be used at one’s discretion.
Absolutely agree
A huge pet peeve I have is when IDEs will automatically replace `.` with `->` for types that overload it like smart pointers and optional because they want to be "helpful" and it actually makes my life significantly harder.
If I wanted an arrow, I'd type an arrow. It's one more character and not that much effort. Who's saving significant time and effort by not thinking about when to use dot vs arrow?
That's part of why I prefer using Vim. There's very little of that annoying garbage and it's generally easier to turn off than with a GUI program.
Occasionally, I would have the brain worm of "Why C++ doesn't have an overloadable operator." then realize this
Bjarne did want this, and has wanted it since D&I isocpp.org/blog/2016/02/a-bit-of-background-for-the-operator-dot-proposal-bjarne-stroustrup
This is a nasty one. I once spent 2 hours debugging a similar situation, although my unique_ptr was pointing to a custom object that had reset(). After this incident I have started to name my reset functions differently.
I think it depends on the code that follows the reset. If it seems incorrect, perhaps the other form of reset was meant.
When writing ptr. or ptr-> IDE will suggest possible options for methods to call, from this you can conclude what exactly you are referring to.
We have in our style guide that methods should not be called `reset` for exactly this reason!
I prefer to do this: value=nullptr
This is one of the downsides of using ‘auto’ imo. This is a much more obvious mistake when the type is defined.
This reminds me of the old debate about whether std::optional should be able to contain reference types (it can't). If allowed, it becomes ambiguous whether the assignment operator assigns to the std::optional itself or the referred-to object. (See Stack Overflow question "Why does std::optional not have a specialization for reference types?")
Except that one was solved long before anyone even suggested the possibility that "assign through" was a valid implementation technique. Rebinding is the only feasible strategy for implementing `std::optional`. See: "To Bind and Loose a Reference" for more details on that.
Thankfully, P2988 - "std::optional" is on it's way to being accepted, putting an end to this phantom counter argument against the only version of `optional` that has ever been implemented.
@@Nobody1707 That sounds promising indeed.
if resetting the underlying object, I would set an auto variable to the pointer to the variable by calling std::unique_ptr::get() and then dereference that with a call to reset(). This makes it clear to a reader what the intention is, and the compiler will optimize the code to the second call in the example.
It reminded me somewhat similar problem with passing `std::shared_ptr` as function argument as const ref or const value. Passing it like `void foo(const std::shared_ptr& val)` gives wrong impression that the object stored by this `shared_ptr` is constant. In fact the pointer is constant but pointing to thing can be modified.
well, never been bitten by confusion on reset() per smart pointers at least - always have keen awareness of when accessing the smart pointer vs the pointer it wraps because C++ has ver distinct syntax for dereferencing a pointer.
And there has often been discussion that maybe there should just be '.' operator for dereferencing to a field or method, but in this case it's helpful to have a distinct operator for dereferencing pointers to fields or methods.
okay, you've just added another thing I'll be needlessly concerned about, thanks I guess 🤷
It's similar to `ptr = 0` vs. `*ptr = 0` or `**ptr = 0`.
Interestingly Rust has a similar ambiguity, though you don't even have the -> operator to tip you off! Luckily, there is an idiom to make a method that may collide in this way be only callable using fully qualified syntax, i.e. Type::method(receiver) as opposed to receiver.method.
This tactic is called an "associated function" as distinct from a method, or rather, a Rust method is just an associated function in which our first parameter is some variant of the keyword self, and so the compiler can see how to perform the argument re-arrangement to transform a method call into the actual associated function call. C++ has never managed to agree how such a thing should work in C++, it's part of the discussion about UFCS (Uniform Function Call Syntax).
I dont think it's a problem or not-obvious with that specific example, because std::any does not expose a -> operator.
Now try with a std::unique_ptr , with T exposing one of the same functions, that's the ambiguous-at-reading case for me (although it's clear to the compiler).
When preparing for this episode I originally used unique_ptr but had someone complain that was an unrealistic example, so I just tried to come up with something else :)
Much obliged! Thank you for making this a video!
I suppose static analysis could indeed be used to ban this pattern, and require a NOLINT that forces people to think twice, as well as documenting for the readers.
That's exactly what the clang-tidy check about easily swappable parameters does :)
you cant really, since even in this case, having reset methods both in the wrapper class and in the wrapped value class is totally coincidental. Unless you gonna ban every usage of that wrapper\wrapped value pair, that can be potentially problematic. Feels like "there's a nasty spider, let's burn the whole house" kind of solution
@@Raspredval1337 You can programatically check that the wrapped/wrapper class both have the reset() method, and issue a warning if the method is called. No need to hardcode a list of possible problematic pairs (if that's what you meant).
Sure, such a warning does not mean the code is wrong. One could see them as False Positives and perhaps the check wouldn't be very useful.
But that would not be the point of the check. Instead, it would be like a STOP sign when driving - you may drive, but you need to stop, add a NOLINT, and document the intention of the code.
This may be overkill for some people/projects, or valuable for others.
I wished you would talk about the case if you have a std::unique_ptr* ptrToUniquePtrToAny!
I'm glad I *didn't* talk about pointers to unique_ptrs :D
I kind of remembering touching this on Twitter/X. Wondering if it was me. Anyway... I am reading good comments about using Clang Tidy to ban this sort of behavior in scenarios where Container and type T, both, contain reset methods (user-defined or not). Banning/Flagging all these cases would leave to many false positives or something like but, what if: you check the following lines? If you do reset on the container (e.g. unique_ptr) and then you keep using the container as if nothing happens, that can be checked, right? The other way around seems more difficult (IMO), because you can reset the underlying type and still have defined behavior.
This is an example of the tradeoffs of collapsing levels of indirection. Such problems aren’t even limited to programming. I’ve seen similar apparent ambiguity in legal documents and manufacturing bills of materials, for example. In each of the cases, the onus has to be on the reader to inform themselves of how to correctly interpret the reference. Putting the onus on the author to write such things that are self-evidently unambiguous to uninformed readers means writing in a way that is unreadably cluttered with details.
Use runtime assert more often!
Just remember to write your unit test and you will catch it!!!
I like Rust's approach (which they unfortunately didn't take from the very beginning) where operations on smart pointers aren't methods but associated functions.
So instead of using `let x = Rc::new(3); let y = x.clone();` where `x.clone()` is ambiguous, you'd call `let y = Rc::clone(&x);`
Smart pointers are pointers - generic code (templates) should work the same for them as normal pointers.
i'd try not to use the method names from unique_ptr in my own types because I know my types are likely to be used in a unique_ptr eventually.
I also have the opinion that operator overloading and conversion operators are usually not as good an idea as they initially seem to be and should just be avoided. and this is another example of that. i.e. overloading operator-> on your own type makes it confusing to use with pointers and you're probably going to use it with pointers eventually.
Honestly though, I don't like the popular internet answer "skill issue", but I think it applies here. Pointers to things can be nested and you should know what level of pointer or thing you're working at, because nested pointers will always have the same pointer methods and operators
They could call it free() instead of reset.
But it doesn't (always) free the resources. The other use-case is to assign a new object, that's why 'reset'. It's just the short form of 'x.reset(nullptr);'
And any user defined class can have a "free" member function, so we're back at square one.
@@danielmarquardt983 well. That's a bad API design right there.
An argument could be made that the two methods should have different names. The problem is that "reset" is too generic. We could choose a different name for each method but we would probably run out of names and it would make the language harder to learn. The natural choice for the method name is to make it reflect what it actually does on a per-class basis. On the other hand, we don't want to unnecessarily expose implementation details even in method names. I think we are stuck with this problem. Better to solve it with a compiler that detects the potential problem and warns the programmer. AFAIK, that would be a level higher than what compilers do these days.
I think the problem is all of the std classes that overload operator->. Renaming reset wouldn't do anything, because any name can be used in a user defined class, so maybe the only solution is a linter rule which forbids calling operator-> on unique_ptr/any/optional/etc. to make sure this doesn't occur.
@@TsvetanDimitrov1976 I view the "reset" issue here as just an example of a much bigger problem. Though, as I think you are trying to point out, there would be no problem if the pointer wasn't a smart pointer and, therefore, didn't have methods like "reset". Still, I don't think we should abandon smart pointers in order to solve a problem like this one. After all, it is only a minor inconvenience that occasionally a programmer will get it wrong and have to debug it. Perhaps we should just consider it a form of job security. LOL
IMHO this results from poor naming. "reset" is far from being specific about what it does in any case.
I would argue for either forcing a reset of the pointer to necessitate calling the function with an argument, i.e. no default arguments, make it explicit; or alternatively renaming the function as it doesn't make as much sense with that name as it could with another. I would also suggest that maybe a simple wrapper class shouldn't use the same idioms as a full-blown container class, but maybe that's just me thinking that.
obvious at the start and concerning at the end(
its the problem of the language if its easily ambiguous and complex. use an easier language if its so complex that you have to teach how to use it. ie k.i.s.s. it will go wrong if it can go wrong. yep having the machine-assembly language-like pointer variables and non-pointer variables just makes things more complex. for no reason. just use references like a sane usable level language would do. yep c/c++ is a mid-level language, not actual high-level language. the more you have to fiddle with low level features like memory and pointers, the lower the level of the language. not even if its ~generally high level. the more maintenance and thinking you have to do on the language itself, the lower level it is, you are no longer programming the app, you are programming about the language itself. focus matters.
valid use for a comment? ;o)
To be honest, with C++ even the comments may be footguns...
Probably this video is a reason to name things differently, ie unique_ptr::reset should have been unique_ptr::set_to_nulptr or something like this.
(*value.get())->Reset();