I am about to start creating my own library for learning purposes! Exciting! It's basically a library for stats/math.
I wanted to ask the forum if yall had any advice on going about creating a library.
If my library is going to use templates, should I just make a header only library (since templates must be declared when they are defined)? What are the drawbacks of this? Currently I have a namespace in my library .hpp file and I am putting all of my template functions in it. Is this a bad idea? Is it okay if the file grows large? Is there a way to separate the difference template functions and group similar ones together in difference files? (I guess I could just have the header file include other sub-topic header files?)
I would assume that a header only library would run faster than a static or dynamically linked library because of compiler optimization?
Anyway, let me know if you have any advice on how to create a new library for a beginner! Are there specific aspects of C++ libraries that are an industry standard? Is there a best-way to design the layout of a library? etc. :)
> If my library is going to use templates, should I just make a header only library
> (since templates must be declared when they are defined)?
If you can do everything as templates, why not.
> What are the drawbacks of this?
The only immediate downside to massive header files is compilation time.
On the upside, you don't have to provide build instructions for X * Y combinations of operating systems and compilers.
> Currently I have a namespace in my library .hpp file and I am putting all of my template
> functions in it. Is this a bad idea?
It's an excellent idea, if your namespace name itself is unique.
> Is it okay if the file grows large?
It's an issue that doesn't seem to worry boost libraries.
> Is there a way to separate the difference template functions and group
> similar ones together in difference files?
Sure, you can slice and dice the internals however you want, and bring everything together in a single .hpp at the top.
> I would assume that a header only library would run faster than a
> static or dynamically linked library because of compiler optimization?
Having visibility of more code does give the compiler more choices.
It might be a 95% win, but I wouldn't presume that it would be a 100% win.
There will always be weird edge cases where the other choice would be faster.
You should focus on producing code that is readable, testable, maintainable and that gives your library users the best chance of success with the least effort.
@salem c has you covered, but I thought I'd chime in on how great an idea this really is. It is more about habit than the specific libraries you're going to create at first draft.
I've been a developer for decades, and in the late 80's I started doing this. When C++ 2.0 was released, templates were new. The compilers of that era were really cramped because 32 MBytes of RAM was about $2,000. Templates were barely possible, and the STL did not yet exist.
One of the first things everyone I knew picking up C++ back then started first by making our own smart pointers (usually reference counted). The second was some kind of container (often AVL trees, queues, stacks, arrays). Hardly any were really good, but that wasn't what mattered at first.
We used them. They crashed. We fixed them. Those libraries grew. HP did that and created the STL.
At first the STL wasn't really usable. It was good, but the compilers couldn't really handle it. We used our "home grown" libraries for a while (I say we because for decades I've been a consultant, working with various teams).
Eventually we replaced some of our "home grown" libraries with the STL, but we noticed some things that were key.
First, the STL didn't have std::shared_ptr. Although it had good containers, our smart pointers had not yet been replaced until Boost provided it.
When std::shared_ptr was brought into the standard (through TR1 at first), we replaced most of our shared pointer usage, but an interesting issue emerged.
The TR1 version did not have std::make_shared. It took a while for that gem to make it's way into the standard, so Boost's std::shared_ptr had better performance.
Even then some use cases favored a particular "home grown" referenced counted pointer. std::shared_ptr, to this day, is not "thread safe" for writing. This makes it seriously prone to a rare bug developers can't notice as obvious.
Say you use a std::shared_ptr to hold a common resource used by multiple threads. The plan is that this resource may be replaced now and then by a thread managing the resource (later a GUI will pick it up to refresh a display, for example). The consuming threads will then update by using this replacement.
The problem happens in that rare moment where one thread changes the std::shared_ptr at the same moment a thread (or multiple threads) are reading from it. That's what doesn't work all the time. It is particularly insidious because this fails maybe 1 in 1,000 (sometimes 1 in 100,000) "exchanges" of the resource by the writing thread. It works in debug, works in unit tests, but fails when thousands of customers use it.
Worse, still, is that the bug causes crashes in code far removed from the source of the problem. It is terribly difficult to recognize.
This happens because even though the reference count of shared_ptr IS WRITE SAFE, the internal copy of the instance it points to IS NOT protected to be thread safe.
This means the reading threads can increment/decrement the reference count of the correct node, BUT END UP WITH THE WRONG POINTER to the instance.
The solution, with std::shared_ptr, is to enforce that a lock (likely a mutex) is used to protect the std::shared_ptr so this situation can't even happen. That's a performance issue, but at least it is made safe. The reason is that there are two things to protect, not one. When merely changing the reference count, that is "automatically" thread safe because that count is an atomic integer, increment/decremented by atomic operations that are automatically thread safe. The internal copy of the pointer to the instance, however, is a second step. The atomic operation of the reference count doesn't protect it, so it can end up pointing to a reference to the wrong instance.
That crashes when that pointer is to an instance subsequently deleted, entirely out of synchronization with the source of the problem.
Here's the point relative to your goal.
That home grown reference counted pointer (in a particular library, in the library of one client of mine, which I developed for them) solved this problem. It had a locking version of the smart pointer. It was "thread safe" for writing by default.
To this day they still use this custom smart pointer in their work, though it has been adapted to work well with the STL over the years. It works without a mutex (even internally). It uses a unique locking mechanism, portable to all operating systems they target (including Android/iOS/Linux/Windows/MAC/embedded). It requires no special adaptation to work on each platform (which is not something you can say about a mutex). They continue to use it not just because it is "automatic" for write safety, it is faster (by several orders of magnitude) than using a mutex to protect a std::shared_ptr.
It goes one better, too. It puts it's own version of "std::make_shared" on steroids. In case you're not familiar, you can use std::shared_ptr with "new" when creating objects. It is known to be less efficient (space and speed) because the there are two objects created, one for the instantiated class, and another for the node used by std::shared_ptr to manage the object's reference count. The "std::make_shared" method unifies this node-class allocation into one block of RAM instead of two. This saves space, saves time (allocating) and is faster when deleting. It is why we use std::make_shared.
The "home grown" smart pointer takes this up a level, where custom allocation of the node/class structure can be drawn from a private, thread aware heap so there can be thousands of "new" objects created out of one memory allocation call. This is faster and smaller than std::make_shared, plus it is "thread aware", meaning that the allocations that do happen work much, much faster in threaded code.
None of that would have happened had it not been part of a habit established of creating and using a "personal" or "corporate" libraries.
The benefits do not emerge over short time frames. They become apparent after years. I've had the fortune of watching that happen over decades in several firms. It is nearly magic, and occasionally some of those things become public, like the STL itself.
I don't recommend a library over the standard library, or to solve problems already well solved by the standard library, or Boost, or something else (like Eigen).
However, you will frequently generate code that is likely applicable in generic form to various situations, and those things should be in your personal library, or any corporate library, over time.
I've known great engineers/programmers who don't have them.
Many have no idea how important this can be. At first, many will scoff at the idea this could even be useful. There are so many great libraries for so many problems, they really don't see how it can be anything but poor, brittle code.
I can tell you there's partial truth to that. Some of those smart pointer libraries turned out to be awful. Not because they failed, some worked very well, but they were not written in a way that protects against misuse. Libraries take on serious responsibility. A cautionary tale on failure for such a thing is found in the history of std::auto_ptr.
Where it does count, though, is when a body of work develops applicable to a wide range of use case. For example, Adobe created their own internal way of presenting dialogs in their user interface. It was their own version of something like Windows Forms. It finds use across all Adobe products (I don't know if they've dropped it in recent years, but it was a staple for them for a long, long while).
AutoDesk has a wide range of CAD products, some very different from their other products, some purchased (they buy their competitors). They have a wide range of rather detailed libraries for much of the complicated work of representing dialogs, panels, user interfaces, 3D data, translators....lots of things.
Keep it up. Make it a habit. Let it follow you through your career.
Wow!! Thank you for the awesome response! I actually had no idea that shared_ptr had those issues. It sounds like you really know your stuff. Hopefully I can learn a lot building this library and, like you said, add some code snippets/knowledge to my repertoire. I'm always open to new tips and advice, so send me a PM if you ever think of something that a beginner trying to break into the industry would benefit from! And again, thank you! :)
To ask one question: I noticed that Salem said,
> If my library is going to use templates, should I just make a header only library
> (since templates must be declared when they are defined)?
If you can do everything as templates, why not.
If I want to use a combination of templates and regular functions/classes in my library, is it still a good idea to make a header only library? Or is there some downside when using both templates and non-templated functions/classes?
Templates by their nature must be "inline" (that is, in header files) unless you use a technique called explicit template instantiation. That is hardly used these days, but when 32 Mbytes (not Gbytes) of RAM was $2,000, compilers were so squeezed we had no choice (Windows 95 was new).
If functions are small, it's ok for that to be in the header of a non-template class, but it can get to be a compiler burden eventually. You'll notice that Boost has a lot of template only libraries, but some must be built because they do have non-template (and non-member) code, which then creates a set of static or dynamic libraries to incorporate.
Don't focus too much on making header only libraries over convenience. Non-template code just naturally creates stand alone translation units (binary static libraries or dynamic), and it saves build time to have them.
One thing to pay some attention to is code in a template class that does not mutate on type. It happens when you're not thinking about it. You may have a function that does some internal thing which doesn't even relate to the type the user provides for the template. If that code is large enough it makes for needless duplication. Sometimes a template is derived from a non-template base so that code which doesn't care about the type (and the data involved which also doesn't care about the type) provided by the user can be in a non-template base class.
Notice how the STL algorithm library is not a collection of classes. They're non-member functions (stand alone functions), though most are template functions that mutate on the types provided. This doesn't mean the algorithms is non-object oriented. It is. However, making those functions non-members allows them to be applicable to a variety of containers, and this allows flexibility in applying, say, sort to various containers.
std::swap is another example of that thinking.
Object oriented development isn't about making classes. Classes are a means of implementing new types, sure, but the idea of objects is about selection of method based on type. That's how swap is object oriented even though it isn't a class.
Another thing to keep in mind when making header only code is the real meaning of the keyword "inline".
Most people assume that is about optimization, but it isn't. It may give a hint to the optimizer, but the primary purpose for the inline keyword is when code (a function in particular) is in a header file. If that file, containing a function definition, is included in multiple translation units (multiple cpp's), the linker sees it duplicated in each one, and complains. The inline keyword informs the linker to expect the duplicates, ignore them and resolve to only one copy of that function in the output.
All member functions defined inside the class declaration are assumed to be inline from this perspective (otherwise all of those functions would create linker complaints).
Thanks for your enthusiasm about my post - I type fast and that was stream of thought. After 40+ years at this I think I did learn a few things ;)
Nice story, Niccolo, I also worked with many custom smart pointers in old codebases. Couple side-nodes:
Niccolo wrote:
one thread changes the std::shared_ptr at the same moment a thread (or multiple threads) are reading from it
For that standard C++ had atomic operations on shared pointers in C++11 and atomic shared pointers in C++20.
where custom allocation of the node/class structure can be drawn from a private, thread aware heap
std::allocate_shared allows a custom allocator (though of course if someone's library is invested in their own incompatible allocator design, it may not work out)
Nice story, Niccolo, I also worked with many custom smart pointers in old codebases.
We all did.
I may be (as usual?) the weirdo who had a dumb-pointer class as well. All it did was delete itself if it went out of scope. Not ideal, but it was fast, and it got rid of some memory leaks a new guy injected into our code base.
heh, if only we could all have all that time back spent doing STL stuff by hand. How many hand-rolled string classes.... how many dynamic array grow/shrink little containers...
I would split it up. Monoliths are slow to compile and harder to debug and test and organize and pull in all kinds of crap you don't need into a program that the compiler discards when it sees you didn't use it.