Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Post
  • Reply
ShoulderDaemon
Oct 9, 2003
support goon fund
Taco Defender

PDP-1 posted:

Weird question: does the first function called in a C++ program absolutely have to be called main, or can we rename it to something else?

From a technical perspective: the system has a rule about how to start a program that typically does not look at symbol names at all. In Linux, this is by looking at the "entry point" address which is encoded in the header of the ELF image that a compiled program is stored as. In an embedded system, it might just be "whatever is at address 0x0000" or some other magic number.

Typically what is actually happening under the compiler is that there's a piece of the C runtime (often called the CRT) which will actually be at this address. Often there is a symbol for this, most typically _start. That is a little piece of hardcoded logic which calls whatever is linked as "main" after doing a few other quick bits of setup like making sure the stack and initial heap pointer are setup how C is expecting.

If you're targeting an embedded system, you can probably completely control this if you're willing to dive into the documentation for your toolchain. So no, things don't have to be called main, as long as you're willing to work out how the beginning-of-execution dance works for your target.

Adbot
ADBOT LOVES YOU

Nalin
Sep 29, 2007

Hair Elf

ShoulderDaemon posted:

If you're targeting an embedded system, you can probably completely control this if you're willing to dive into the documentation for your toolchain. So no, things don't have to be called main, as long as you're willing to work out how the beginning-of-execution dance works for your target.

I believe its usually a linker option to change the entry point. But yeah, doing that will most likely cause a linker error when it tries to link to your runtime library.

Dijkstracula
Mar 18, 2003

You can't spell 'vector field' without me, Professor!

PDP-1 posted:

Weird question: does the first function called in a C++ program absolutely have to be called main, or can we rename it to something else?

The C++ standard, § 6.9.3.1 posted:

A program shall contain a global function called main attached to the global module. Executing a program
starts a main thread of execution (6.9.2, 32.4) in which the main function is invoked, and in which variables
of static storage duration might be initialized (6.9.3.2) and destroyed (6.9.3.4).

An implementation shall not predefine the main function. This function shall not be overloaded.

Of course in the world of embedded programming the rules are made up and the points don't matter anyway. You could, if you truly wanted, wrap each main() behind a preprocessor guard that checks for the compilation target, but fundamentally the compiler will still only ever see one main() function.

nelson
Apr 12, 2009
College Slice
Here’s a simple question. Lets say you have 2 data structures containing pointers. One is a vector containing pointers to all the objects in question. The other is a queue that may hold 0 or more at any given time. Is it better for the vector to have unique pointers and the queue to hold raw pointers, or is it better to use shared pointers for both? This is a single threaded application, would the answer be different for multi threaded?

Dijkstracula
Mar 18, 2003

You can't spell 'vector field' without me, Professor!

My personal opinion is that it's generally best to be consistent with what level of intelligence your pointers have - feels to me that a value pointed to by both a unique_ptr and a raw pointer is no safer than two raw pointers. You may wish to investigate whether a weak_ptr suits your needs better than just a raw one.

nelson posted:

This is a single threaded application, would the answer be different for multi threaded?
Depends on whether these two structures, and/or the objects to which they point, remain local to one thread in the hypothetical multithreaded application. If not, then functionally this part of the program is single-threaded and it doesn't matter what the other threads are doing. If so, then shared_ptr's reference count is thread safe -- that is, you don't need additional locking to make sure they behave correctly -- but that might not be true of whatever they point to, so you shouldn't have to worry about two threads firing its destructor at the same time but in general no smart pointer will make race conditions unrepresentable.

leper khan
Dec 28, 2010
Honest to god thinks Half Life 2 is a bad game. But at least he likes Monster Hunter.

nelson posted:

Here’s a simple question. Lets say you have 2 data structures containing pointers. One is a vector containing pointers to all the objects in question. The other is a queue that may hold 0 or more at any given time. Is it better for the vector to have unique pointers and the queue to hold raw pointers, or is it better to use shared pointers for both? This is a single threaded application, would the answer be different for multi threaded?

What semantics of ownership over the resource do you want? If the vector removes a pointer, do you want the pointers in queue to become invalid? Does your vector reach into the queue or is this the caller's problem?

Answer the questions on ownership, and the correct set of pointer types should become obvious.

ultrafilter
Aug 23, 2007

It's okay if you have any questions.


PDP-1 posted:

Weird question: does the first function called in a C++ program absolutely have to be called main, or can we rename it to something else?

The reason I ask is because I just started some work on an embedded microcontroller with an asymmetric dual core, one is a (relatively) beefy Cortex M7 and the other is a weaker Cortex M4 on the same silicon. When kickstarting the chip from power on and doing all of the stuff to init the processors before handing off fully to the software side I end up with two functions, both called main(), when a more natural naming might be main_m7() and main_m4(). My IDE absolutely rejects me naming them that, but having two different things with the same name is mildly confusing in the code.

I suppose I can just make each main() call a pass-thru function to main_m7 and main_m4, but it's just an interesting thought experiment to consider naming them something else natively.

You can as others have shown, but if this is code that anyone else will ever need to read without talking to you, you'd need to think very carefully about how to document that the entry point is not what they're expecting. Doing something with the preprocessor is a lot more intelligible.

nelson
Apr 12, 2009
College Slice

leper khan posted:

What semantics of ownership over the resource do you want? If the vector removes a pointer, do you want the pointers in queue to become invalid? Does your vector reach into the queue or is this the caller's problem?

Answer the questions on ownership, and the correct set of pointer types should become obvious.

The vector is just the complete data set initialized from a file. Nothing is added or deleted once the file loading is done, although the objects pointed to can be modified in a dynamically determined order, which is what the queue is for.

There’s really no “need” to use smart pointers at all but I’m an old guy trying to understand best practices when it comes to modern C++ development.

nelson fucked around with this message at 16:38 on Dec 12, 2023

leper khan
Dec 28, 2010
Honest to god thinks Half Life 2 is a bad game. But at least he likes Monster Hunter.

nelson posted:

The vector is just the complete data set initialized from a file. Nothing is added or deleted once the file loading is done, although the objects pointed to can be modified in a dynamically determined order, which is what the queue is for.

There’s really no “need” to use smart pointers at all but I’m an old guy trying to understand best practices when it comes to modern C++ development.

Modern types to use would be shared_ptr in vector with weak_ptr in queue.

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
If you don't know what the ownership story is, figure out what it is before you start worrying about exactly what type of pointer to use. The ownership story tells you what pointer is correct.

For example, if the vector owns the data elements and they should be deleted at the same time the vector is, and all other references to those elements should have already been released by the time the vector is destroyed, then using unique_ptr in the vector (to express that ownership) and raw pointers everywhere else (to express that lack of ownership) is appropriate. If that's not the ownership story, then you want something different.

nelson
Apr 12, 2009
College Slice

leper khan posted:

Modern types to use would be shared_ptr in vector with weak_ptr in queue.

Why?

Jabor posted:

using unique_ptr in the vector (to express that ownership) and raw pointers everywhere else (to express that lack of ownership) is appropriate.

Sounds logical.

leper khan
Dec 28, 2010
Honest to god thinks Half Life 2 is a bad game. But at least he likes Monster Hunter.

nelson posted:

Why?

Sounds logical.

Because you have a shared resource with an owner and non-owners. The weak_ptr will help you if the lifetime of the data in the vector changes. If it won't, unique + raw or raw + raw will also obviously function.

nelson
Apr 12, 2009
College Slice

leper khan posted:

Because you have a shared resource with an owner and non-owners. The weak_ptr will help you if the lifetime of the data in the vector changes. If it won't, unique + raw or raw + raw will also obviously function.

The way I ended up implementing it was shared + shared. I’ve never used a weak pointer before, maybe I should learn more about those.

Dijkstracula
Mar 18, 2003

You can't spell 'vector field' without me, Professor!

nelson posted:

The way I ended up implementing it was shared + shared. I’ve never used a weak pointer before, maybe I should learn more about those.

From what you described, shared + shared sounds semantically correct, so not a bad approach

Phobeste
Apr 9, 2006

never, like, count out Touchdown Tom, man

ultrafilter posted:

You can as others have shown, but if this is code that anyone else will ever need to read without talking to you, you'd need to think very carefully about how to document that the entry point is not what they're expecting. Doing something with the preprocessor is a lot more intelligible.

One of arduinos uncountably many crimes is this poo poo

Foxfire_
Nov 8, 2010

PDP-1 posted:

The reason I ask is because I just started some work on an embedded microcontroller with an asymmetric dual core, one is a (relatively) beefy Cortex M7 and the other is a weaker Cortex M4 on the same silicon. When kickstarting the chip from power on and doing all of the stuff to init the processors before handing off fully to the software side I end up with two functions, both called main(), when a more natural naming might be main_m7() and main_m4(). My IDE absolutely rejects me naming them that, but having two different things with the same name is mildly confusing in the code.
Do you have an operating system with concepts like 'programs', or are you just using the chip directly?

If direct, the reset vector run by the hardware will just lead to some sram address. Typically you will have a handwritten assembly program there that
- sets up clocks
- sets up any nondefault memories
- copies data from flash to any initialized globals in your c++ program
- zeroes memory for any zero initialized globals
- calls constructors for c++ globals
- jumps to main

"main" there isn't special, you can name it whatever you want (or call other c/c++ functions earlier if you'd rather do some setup there and are careful about what's available at that point)

PDP-1
Oct 12, 2004

It's a beautiful day in the neighborhood.
Thanks everyone who commented, I appreciate the info and advice!

Foxfire_ posted:

Do you have an operating system with concepts like 'programs', or are you just using the chip directly?

Nope, this is bare metal register-flippy stuff. The power comes on and the hardware knows the first four bytes are the starting value of the stack pointer, then the next four bytes are the reset vector aka the starting value of the program counter. Those get loaded and then you pop into the reset vector code where I do all the setup stuff you described, then call main.

So yeah, I didn't think the name main was going to be special in that context, it's just a thing I call once the chip is ready to go. But I think the IDE is throwing a fit because, as Dijkstracula pointed out, the name main is standardized as special in most cases.

Anyway I think I hit on a solution that meets my needs without getting into doing anything too strange. The function name main may be special but the file name that main is located in is not. So I should be able to call one file main_m4.cpp and the other main_m7.cpp, put each of the respective main() calls in each one which keeps the compiler happy, but now I'm also happy because I don't have two tabs open in the IDE labeled main.cpp but containing different contents causing me to swear under my breath as I open the wrong one for the hundredth time that day.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

nelson posted:

The vector is just the complete data set initialized from a file. Nothing is added or deleted once the file loading is done, although the objects pointed to can be modified in a dynamically determined order, which is what the queue is for.

There’s really no “need” to use smart pointers at all but I’m an old guy trying to understand best practices when it comes to modern C++ development.
If the vector outlives all other users of the things (i.e. it is the undisputed owner of that data), then std::unique_ptr in the vector and std::reference_wrapper in the list would be what the open source community I participate in would recommend. reference_wrapper is basically the same as a pointer at runtime, but semantically different in that it's invalid for it to be nullptr, which if your list is always pointing at something means this type would be more accurate. However, it's also syntactically more painful to use than a bare pointer, and mostly doesn't even act like a reference. (I found it pretty disappointing when I first used it, IMO it would be more accurately named "not_null_pointer".)

shared_ptr and weak_ptr carry a performance cost that in some circumstances can be significant. I tried turning a horrible construct that used unique_ptr with a custom destructor to make some values shared singletons and others deleteable, into a shared_ptr so the singletons could just duplicate the existing pointer and the deleteable ones would be allocated, because the weird construct was hideously hard to follow and had a whole bunch of issues with having to declare specified destructors everywhere, but the performance with shared_ptr was 3x slower. I did manage to clean it up in the end, but mostly leaving the underlying structure alone and just putting some extra wrappers around it to make it less gross at the use-sites.

roomforthetuna fucked around with this message at 01:45 on Dec 13, 2023

OddObserver
Apr 3, 2009
It looks like std::shared_ptr requires barriers on refcount decrement, plus also potentially another allocation if you are holding it wrong.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

OddObserver posted:

It looks like std::shared_ptr requires barriers on refcount decrement, plus also potentially another allocation if you are holding it wrong.
Yeah, the barriers were most likely the big cost in the specific context. std::move was being done in all the right places to avoid the unnecessary costs, and there's no getting around the one additional allocation if you're actually using it shared-pointerily. The point is shared_ptr (and by extension weak_ptr since it turns into a shared_ptr before you can use it) is significantly more expensive than unique_ptr (which is performance-wise practically identical to a bare pointer) in both time and space.

There is a fairly common construct, the refcounted-pointer, which is like shared_ptr without the barriers, but it doesn't exist in the standard library. And it still wouldn't be necessary or useful for the context under discussion. I should probably have tried using that in the context I was talking about though, it probably would have been the best of both worlds, and there was already an implementation of it in the project in question.

Edit: the other nice thing about mostly using unique_ptr is it gets you into habits that are beneficial for shared_ptr too, because you *can't* forget to std::move a unique_ptr, versus if you don't std::move (when you could have) a shared_ptr then you end up performing extra increments and decrements and barriers.

nelson
Apr 12, 2009
College Slice

roomforthetuna posted:

If the vector outlives all other users of the things (i.e. it is the undisputed owner of that data), then std::unique_ptr in the vector and std::reference_wrapper in the list would be what the open source community I participate in would recommend. reference_wrapper is basically the same as a pointer at runtime, but semantically different in that it's invalid for it to be nullptr, which if your list is always pointing at something means this type would be more accurate. However, it's also syntactically more painful to use than a bare pointer, and mostly doesn't even act like a reference. (I found it pretty disappointing when I first used it, IMO it would be more accurately named "not_null_pointer".)

shared_ptr and weak_ptr carry a performance cost that in some circumstances can be significant. I tried turning a horrible construct that used unique_ptr with a custom destructor to make some values shared singletons and others deleteable, into a shared_ptr so the singletons could just duplicate the existing pointer and the deleteable ones would be allocated, because the weird construct was hideously hard to follow and had a whole bunch of issues with having to declare specified destructors everywhere, but the performance with shared_ptr was 3x slower. I did manage to clean it up in the end, but mostly leaving the underlying structure alone and just putting some extra wrappers around it to make it less gross at the use-sites.

This is very interesting and helpful. Thank you.

StumblyWumbly
Sep 12, 2007

Batmanticore!

PDP-1 posted:

Weird question: does the first function called in a C++ program absolutely have to be called main, or can we rename it to something else?

The reason I ask is because I just started some work on an embedded microcontroller with an asymmetric dual core, one is a (relatively) beefy Cortex M7 and the other is a weaker Cortex M4 on the same silicon. When kickstarting the chip from power on and doing all of the stuff to init the processors before handing off fully to the software side I end up with two functions, both called main(), when a more natural naming might be main_m7() and main_m4(). My IDE absolutely rejects me naming them that, but having two different things with the same name is mildly confusing in the code.

I suppose I can just make each main() call a pass-thru function to main_m7 and main_m4, but it's just an interesting thought experiment to consider naming them something else natively.
How are you building this? I'd think you'd need 2 separate projects and linkers, but it sounds like your IDE is putting it all into one? Or is it mainly that you don't like having 2 similar projects open at once?

Plorkyeran
Mar 22, 2007

To Escape The Shackles Of The Old Forums, We Must Reject The Tribal Negativity He Endorsed

roomforthetuna posted:

Yeah, the barriers were most likely the big cost in the specific context. std::move was being done in all the right places to avoid the unnecessary costs, and there's no getting around the one additional allocation if you're actually using it shared-pointerily.

std::make_shared avoids the extra allocation, at the cost of making the allocation stay alive as long as there's any weak_ptrs pointing at it.

PDP-1
Oct 12, 2004

It's a beautiful day in the neighborhood.

StumblyWumbly posted:

How are you building this? I'd think you'd need 2 separate projects and linkers, but it sounds like your IDE is putting it all into one? Or is it mainly that you don't like having 2 similar projects open at once?

This is in Visual Studio with the VisualGDB extension for working on embedded microcontrollers. I'm in the learning phase of how to do dual core processors but as I understand it now you make one VS solution with two projects inside it, one for each processor. Then each project gets its own linker/startup/chip header/code that can be compiled and downloaded to the target independently.

The two project thing is what leads to having two main.cpp files which gets slightly confusing since both can be open in the IDE at once. I think I can resolve that by naming the files main_m7.cpp and main_m4.cpp, and let each contain a main() call without doing anything super weird.

Next up on the list is figuring out how to assign different hardware peripherals to each processor and how to run a hardware debugger with two processors since apparently you can set it up so that a breakpoint in one stops only that processor or both.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

Plorkyeran posted:

std::make_shared avoids the extra allocation, at the cost of making the allocation stay alive as long as there's any weak_ptrs pointing at it.
I was meaning in the context of the one that's being used as a singleton, where every instantiation of a "copy" has to allocate an additional shared_ptr object. But now that I think about it that's true of the singleton-special-unique-ptr-with-do-nothing-destructor too. And both of those are probably on the stack anyway.

But that's interesting, I didn't realize weak_ptrs keep the allocated object alive if it's been made with make_shared. It makes sense now you say it, and I guess is mostly unimportant since the weak_ptrs should all eventually get destroyed too anyway, but could be important for a big enough object.

Volguus
Mar 3, 2009
Umm, no, I don't think weak ptrs keep anything alive. The entire point of the weak ptrs is that you don't need the object to be alive by the time you're using it. When you do need to use the object, then you do weak_ptr.lock() which gives you back a shared_ptr which may or may not be valid and you have to test for that. If the shared_ptr you get back is valid, then you just use it. Otherwise no, it was already destroyed.

If you absolutely need to be alive by the time the callback is called (you want to keep yourself alive), you have shared_from_this for that.

Volguus fucked around with this message at 04:56 on Dec 14, 2023

Nalin
Sep 29, 2007

Hair Elf

Volguus posted:

Umm, no, I don't think weak ptrs keep anything alive. The entire point of the weak ptrs is that you don't need the object to be alive by the time you're using it.

You know, I thought the exact same thing so I checked cpp reference:
https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared

From the notes:

quote:

This function may be used as an alternative to std::shared_ptr<T>(new T(args...)). The trade-offs are:

std::shared_ptr<T>(new T(args...)) performs at least two allocations (one for the object T and one for the control block of the shared pointer), while std::make_shared<T> typically performs only one allocation (the standard recommends, but does not require this; all known implementations do this).

If any std::weak_ptr references the control block created by std::make_shared after the lifetime of all shared owners ended, the memory occupied by T persists until all weak owners get destroyed as well, which may be undesirable if sizeof(T) is large.

God drat it C++.

EDIT: It will still properly call the destructor on object T. It's just that when using make_shared, the control block and the managed object are created in the same memory allocation and object T is instantiated with placement new. So the object should get deleted with placement delete, but since the control block was also part of the allocation, you are left with a super large control block.

Nalin fucked around with this message at 05:07 on Dec 14, 2023

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
the weak pointer doesn't keep the object itself alive, but it does keep the little chunk of memory containing the refcount alive - so it can know that the object it points to is dead.

and i think what plorkyeran is saying is that if you used make_shared then the refcount ends up in the same allocation as the object itself - so while you run the destructor when the last shared_ptr is gone, the actual memory allocation that held that object is still around until all the weak_ptrs are gone as well.

Volguus
Mar 3, 2009

Nalin posted:

You know, I thought the exact same thing so I checked cpp reference:
https://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared

From the notes:

God drat it C++.


Jabor posted:

the weak pointer doesn't keep the object itself alive, but it does keep the little chunk of memory containing the refcount alive - so it can know that the object it points to is dead.

and i think what plorkyeran is saying is that if you used make_shared then the refcount ends up in the same allocation as the object itself - so while you run the destructor when the last shared_ptr is gone, the actual memory allocation that held that object is still around until all the weak_ptrs are gone as well.

Aaah, ok, now I understand the initial statement. Yes, the object is destructed and destructor is called and all, but you're right, the drat thing is still there somewhere. Yeah, you don't want weak_ptr's to be alive for too long.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

Volguus posted:

Aaah, ok, now I understand the initial statement. Yes, the object is destructed and destructor is called and all, but you're right, the drat thing is still there somewhere. Yeah, you don't want weak_ptr's to be alive for too long.
Yeah, I guess it's not a big deal if it's some tree-shaped object with a narrow trunk, because it would do the destructor at least so only the "stump" of the tree would persist, but if it was a broad object like shared_ptr<std::array<char, 1024*1024>> then the weak_ptr to an object made with make_shared would be keeping that megabyte allocated.

Zopotantor
Feb 24, 2013

...und ist er drin dann lassen wir ihn niemals wieder raus...

Nalin posted:

God drat it C++.

That's a bit unfair. The real issue is that typical memory managers used with C++ programs can't free just part of a previously allocated block. That restriction got baked into the language.
OTOH, that's a rather common restriction. The only counterexample I can remember are some Prolog implementations which could partially garbage collect structures when some of their internals were still live.

OddObserver
Apr 3, 2009
I feel like avoiding the second allocation with make_shared is someone trying to be way too clever.

Jabor
Jul 16, 2010

#1 Loser at SpaceChem
Memory locality is important, and putting both the refcount and the actual object in the same cache line (instead of spreading them in different allocations across memory) is gonna be helpful for performance.

It also doesn't have any real downsides if the object is small or if you're not using weak_ptr.

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe
There's several ways to implement weak references alongside strong refcounting, and they each have trade-offs. There's no silver bullet here.

1. You can store a weak reference count in the primary refcount structure. Weak references store a reference to the primary refcount structure. Upsides: creating and destroying a weak reference is as cheap as creating and destroying a strong reference; no extra space usage per weak reference; no extra allocations. Downsides: the primary refcount structure has to store the weak reference count even if you never form a weak reference; you can't deallocate the primary refcount structure until you've destroyed or at least touched all the weak references. If the primary refcount structure is co-allocated with the referenced object (the common case outside of C++), and the referenced object is very large, the memory impact of delayed deallocation is pretty significant.

2. You can store a collection of all the weak references as an intrusive linked list through the weak reference objects. Weak references store a reference to the primary refcount structure, which you zero out by walking the linked list when the strong refcount hits zero. Upsides: the primary refcount structure can be deallocated immediately; no extra allocations. Downsides: each weak reference takes up more space; creating and destroying a weak reference is quite a bit more expensive; the primary refcount structure has to store the head of the linked list even if you never form a weak reference.

1A/2A. You can amend either of these options to move the storage of the weak refcount / weak-references linked list from the primary refcount structure to a separately-allocated secondary refcount structure. Generally you do this by using the low bit of the strong refcount to say that those bits are actually a pointer to the secondary refcount structure, then putting the real strong refcount in the secondary structure. Added upsides: no extra space if you don't make a weak reference. Added downsides: the strong refcount has to be at least pointer-wide; you have to allocate the secondary refcount structure when forming the first weak reference, and you have to decide what happens if that allocation fails; the strong refcount logic gets significantly more complicated; strong refcounts become slower once you've formed a weak reference. Typically, the transition to using a secondary refcount structure is permanent, so even if you only make a weak reference once and fleetingly, strong refcounts are permanently slower from that point. Some of the downsides disappear if you need a secondary refcount structure anyway for other reasons, e.g. to support other crazy APIs like attaching side-storage to an object.

1AA. You can store a reference to the primary refcount structure in the secondary refcount structure, then make weak references store a reference to the secondary refcount structure instead of the primary. When the strong refcount goes to zero, you zero out the self-reference. Added upsides: you can now deallocate the primary refcount structure when the strong refcount goes to zero, avoiding the delayed deallocation problem. Added downsides: the secondary refcount structure gets a little bigger; there's an extra indirection on every weak reference access.

rjmccall fucked around with this message at 00:37 on Dec 15, 2023

Nalin
Sep 29, 2007

Hair Elf

Zopotantor posted:

That's a bit unfair. The real issue is that typical memory managers used with C++ programs can't free just part of a previously allocated block. That restriction got baked into the language.
OTOH, that's a rather common restriction. The only counterexample I can remember are some Prolog implementations which could partially garbage collect structures when some of their internals were still live.

I was mainly complaining about how that specific, very important bit of functionality is something that isn't really known or noticed unless you are reading implementation notes. Especially since many places that teach it basically say its a way to avoid having to ever write "new" in your code. It's just another annoying thing you have to keep in your mind.

EDIT: I guess I'm just annoyed that two valid implementations are not easily distinguished between the two and you have to remember which way of creating the object results in which implementation.

Nalin fucked around with this message at 00:48 on Dec 15, 2023

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe
For what it's worth, Swift uses a combination of those approaches: our weak references are immediately zeroed on destruction and don't block deallocation, but our unowned references just refcount and do block deallocation. A newly-allocated object stores narrow strong and unowned refcounts inline in the object header, but various things like forming a weak reference or overflowing one of the narrow refcounts permanently transition the object to using a secondary structure with wider refcounts and support for weak references. The idea is that we encourage unowned to be used for things like back-references, which is reinforced by the fact that they assert (instead of producing nil) if you try to promote one to a strong reference and the object has been destroyed. Delaying deallocation is generally fine for back-references. weak is the more general-purpose design that always requires an optional type, and we felt that delaying deallocation would be problematic for those use cases.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

Nalin posted:

I was mainly complaining about how that specific, very important bit of functionality is something that isn't really known or noticed unless you are reading implementation notes. Especially since many places that teach it basically say its a way to avoid having to ever write "new" in your code. It's just another annoying thing you have to keep in your mind.

EDIT: I guess I'm just annoyed that two valid implementations are not easily distinguished between the two and you have to remember which way of creating the object results in which implementation.
If you're just using it as a way to avoid writing "new" then you should *always* be using unique_ptr which doesn't really have any of these gotchas, it literally just performs identically to a bare pointer but forcing you to be more careful about ownership. shared_ptr just gets wacky because the stuff its doing is wacky, and it's trying to be everything for everyone (e.g. having barriers in it so it can work with threads, which can be really unnecessarily poor for performance if the pointer ownership doesn't actually traverse threads).

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe
std::shared_ptr is definitely a much more abstract type than you might expect: it can either take ownership of an existing allocation or co-allocate, it defaults to using the standard allocator but can work with an arbitrary one, it supports a bunch of related features around weak references, etc. All of those choices have costs that get paid at runtime, and modern designers would probably force that all to be statically explicit. And maybe they’d be right to; I dunno, though.

roomforthetuna
Mar 22, 2005

I don't need to know anything about virii! My CUSTOM PROGRAM keeps me protected! It's not like they'll try to come in through the Internet or something!

rjmccall posted:

std::shared_ptr is definitely a much more abstract type than you might expect: it can either take ownership of an existing allocation or co-allocate, it defaults to using the standard allocator but can work with an arbitrary one, it supports a bunch of related features around weak references, etc. All of those choices have costs that get paid at runtime, and modern designers would probably force that all to be statically explicit. And maybe they’d be right to; I dunno, though.
It's definitely nice to have a simple does-everything option available for when you're *not* in a particularly performance-critical context, like most things are not. I appreciate the shared_ptr for that, but I do wish there were some other abstractions in the standard library too, especially a refcounted pointer that doesn't support weak or thread safety. (Supporting co-allocation or not is fine since there's no additional performance cost in being capable of both.)

Edit: vvvv I meant supporting "co-allocation or not", like it should be capable of doing both - there's a performance cost in *doing* the worse one, but supporting both means you only do the worse one when that's what the user of the library asked for. As a contrast to supporting thread-safety, weak_ptr or shared_from_this have a performance cost whether you're using them or not. [or, depending on implementation, have no performance cost to not use but are performance-awful when you do use them]

roomforthetuna fucked around with this message at 13:58 on Dec 15, 2023

Adbot
ADBOT LOVES YOU

rjmccall
Sep 7, 2007

no worries friend
Fun Shoe
I mean, a std::shared_ptr that only had to support co-allocated objects would probably be one pointer instead of two. That’s a pretty significant performance cost.

  • 1
  • 2
  • 3
  • 4
  • 5
  • Post
  • Reply