|
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.
|
# ? Dec 12, 2023 06:02 |
|
|
# ? Jun 10, 2024 13:34 |
|
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.
|
# ? Dec 12, 2023 06:09 |
|
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 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.
|
# ? Dec 12, 2023 14:11 |
|
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?
|
# ? Dec 12, 2023 14:56 |
|
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?
|
# ? Dec 12, 2023 15:23 |
|
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.
|
# ? Dec 12, 2023 16:05 |
|
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? 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.
|
# ? Dec 12, 2023 16:14 |
|
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? 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 |
# ? Dec 12, 2023 16:35 |
|
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. Modern types to use would be shared_ptr in vector with weak_ptr in queue.
|
# ? Dec 12, 2023 16:42 |
|
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.
|
# ? Dec 12, 2023 16:44 |
|
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.
|
# ? Dec 12, 2023 16:51 |
|
nelson posted:Why? 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.
|
# ? Dec 12, 2023 16:56 |
|
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.
|
# ? Dec 12, 2023 17:04 |
|
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
|
# ? Dec 12, 2023 17:09 |
|
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
|
# ? Dec 12, 2023 18:09 |
|
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. 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)
|
# ? Dec 12, 2023 18:46 |
|
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.
|
# ? Dec 13, 2023 00:42 |
|
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. 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 |
# ? Dec 13, 2023 01:39 |
|
It looks like std::shared_ptr requires barriers on refcount decrement, plus also potentially another allocation if you are holding it wrong.
|
# ? Dec 13, 2023 01:55 |
|
OddObserver posted:It looks like std::shared_ptr requires barriers on refcount decrement, plus also potentially another allocation if you are holding it wrong. 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.
|
# ? Dec 13, 2023 02:30 |
|
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".) This is very interesting and helpful. Thank you.
|
# ? Dec 13, 2023 03:27 |
|
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?
|
# ? Dec 13, 2023 03:53 |
|
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.
|
# ? Dec 13, 2023 03:56 |
|
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.
|
# ? Dec 13, 2023 04:54 |
|
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. 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.
|
# ? Dec 13, 2023 14:19 |
|
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 |
# ? Dec 14, 2023 04:53 |
|
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: 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 |
# ? Dec 14, 2023 05:01 |
|
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.
|
# ? Dec 14, 2023 05:03 |
|
Nalin posted:You know, I thought the exact same thing so I checked cpp reference: 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. 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.
|
# ? Dec 14, 2023 05:06 |
|
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.
|
# ? Dec 14, 2023 05:25 |
|
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.
|
# ? Dec 14, 2023 21:53 |
|
I feel like avoiding the second allocation with make_shared is someone trying to be way too clever.
|
# ? Dec 14, 2023 21:57 |
|
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.
|
# ? Dec 14, 2023 22:55 |
|
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 |
# ? Dec 15, 2023 00:32 |
|
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. 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 |
# ? Dec 15, 2023 00:33 |
|
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.
|
# ? Dec 15, 2023 00:48 |
|
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.
|
# ? Dec 15, 2023 02:45 |
|
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.
|
# ? Dec 15, 2023 03:31 |
|
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. 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 |
# ? Dec 15, 2023 03:46 |
|
|
# ? Jun 10, 2024 13:34 |
|
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.
|
# ? Dec 15, 2023 06:35 |