This is why C++ usage is still growing fast: almost every new feature makes it possible to write more powerful libraries that get, thereby, easier to use.
This is why C++ usage is still growing fast: almost every new feature makes it possible to write more powerful libraries that get, thereby, easier to use.
LOL no, it's growing because people can't get the performance and battery life they want out of other languages and it's a hugely entrenched player with a gigantic existing ecosystem and is able to seamlessly call into all existing C code as well. Language features after C++11 (arguably C++98) have very little to do with new growth except keeping interest by showing it's alive and spurring book sales and conference tickets. Most libraries still target old versions of the standard and don't use most new features. C++14 vs C++17 vs C++20 makes very little difference for 99% of users. Most people are not using polymorphic allocators to deserialize Unicode inside their constexpr DSL parser.
Most of the use of new features has been to make libraries more powerful, and easy and safe to use. So, users of those libraries don't need to use the new features directly to benefit from them.
Which was my point.
Absent the absolute necessity of generics to support essential semantics, I'm probably in favour of a simpler generics-less version. I don't see why "more powerful libraries" get automatically easier to use. It could often be the opposite.
There is a tradition in some C++ subcultures where they try to cram in as many invariants as possible into types, concepts, templates, etc. at all costs. I tend to think that if all this heavy machinery is needed, the functionality might be too complicated from the start.
You might as well say, "I don't see why a more expensive dinner has to be tastier."
It is always easy to make libraries that are hard to use, in any language, but those do not become popular if there are better alternatives.
We need generics because users have types that they need libraries to work with. Your own example, of an intrusively-linked list, illustrates this: without generics, you could not write a linked-list library component that would be usable for that. This is why C programs are crammed with so many custom one-off hash tables: You cannot code a useful hash table library in C.
Libraries start simple, and accumulate your "heavy machinery" as they are made more useful and usable for their growing family of users. A library that lacks users does not.
It's a matter of tradeoffs. Library design is a balancing act.
> We need generics because users have types that they need libraries to work with.
This is right where I become sceptical. Most libraries shouldn't care for the user's types at all. They should expose their own types so you can work the library - not the other way around.
Please provide an actual use case where the library has to "know" about the user's types (and please don't mention std::sort. It's as common an example as is "Dog :: Animal" examples to argue for class inheritance, and is just as irrelevant).
> Your own example, of an intrusively-linked list, illustrates this: without generics, you could not write a linked-list library component that would be usable for that.
As I mentioned, linked list can tangentially benefit from a very very thing generics layer on top of a fixed data structure implementation. The layer does nothing more than "instanciate" the types, but there is no code generated. But then again, the added convenience / safety is minimal, I once wrote a C++ implementation that I was quite happy with, and never used it.
> This is why C programs are crammed with so many custom one-off hash tables
There are probably not huge issues with a design like "HashTable ht = { .hash = &foo_key_hash, .equals = &foo_key_equals }" (if quick results are what you're after), but yes it is nice to have a few lines of type-safe wrap generated over some generic container interface.
An alternative reason why you'll find a good amount of custom hashtables in C code bases is, I suppose, that there are a lot of different ways to implement hash tables. Also writing a little code here might not be so bad. I've had 1 use case for a "hash" table in 2021 (glyph hash table for a new iteration of my font rendering code) and I've gotten away without even implementing it - the for-loop I've written has never shown up in any performance profile.
Considering that container data structures are probably the highest profile application for generics, I'm still not convinced that generics are needed in a systems programming language...
I have already cited two separate examples, which you have studiously ignored. Look online and find them in their thousands. It is hard to find even one C++ library that is not made better by its ability to integrate with users' choices of types.
This is an utterly bizarre statement. Should people convert collections of Xs from library 1 to other kinds of collections of Ys to work with library 2?
A very basic use case that shows up all the time everywhere, especially in systems programming, is that I have an array of items and I want to pass it to some library. But, to work with an array, you need to know the size of elements of the array, so that arr[x] knows how to compute the address of x. There are exactly 3 ways to achieve this:
1. The library only accepts arrays of some specific type (e.g. int[]). If you have some other type, you have to find some way of converting to the type known in the library - in O(n) time.
2. The library expects arrays of some fixed-size type that can hold any value (e.g. void*[]). This typically adds huge overhead, since now the elements of the array are not together in memory, they are scattered all over the place; and the library must fetch them from memory before doing anything with them.
3. Generics - the library accepts T[], and can easily do sizeof(T) to know how to access arr[x].
Even qsort is actually an example of C's convoluted support for generic arrays: you're explicitly passing the array base and its type (the number of bytes) to qsort. C's extremely basic type system anyway essentially identifies types with byte sizes in practice, so it's not obvious that qsort works like this (and of course, it's less safe, since instead of passing an int, you have to pass void* + sizeof(int), potentially getting them mixed up).
My thinking is this. Either a library function cares about what type of data you pass to it because it is designed to do some operation on it - then it will accept only a specific type, like "void func(Foo *foos, int count)". Or, the function does not care about the type of data but just wants to send it somewhere else. In this case, you can pass a void-pointer + size to it.
Interfaces like sorting functions can be seen as exceptions under the umbrella term "container library". Like the qsort() example, usually all you need is a few metrics for the data type - size, maybe alignment - and at most a few "method calls" - equality, simple get/set operations - and that's it. For all I can say these situations are not very common in systems programming, at least not for what I do. This stuff tends to come up in application level programming. It's definitely very common in my Python scripts. When it does come up in systems occasionally, a vtable struct is a good way to deal with the situation generically. In the qsort() case, the function pointer can be passed directly.
> 1. The library only accepts arrays of some specific type (e.g. int[]). If you have some other type, you have to find some way of converting to the type known in the library - in O(n) time.
Considering how computers work, I don't think it is much of a constraint to assume that arrays are of the form "Foo *foos". Coincidentally C _does_ have builtin "generics" for arrays of things, and real machines do have assembly instructions to quickly compute addresses based on hardcoded element size.
> (and of course, it's less safe, since instead of passing an int, you have to pass void* + sizeof(int), potentially getting them mixed up).
There is little real chance of getting them mixed up. If you actually run the code you will notice.
How do I make sure they always stay in sync ? (adding removing elements)
Because the ever increasing number of CVE's where brilliant programmers who screw this up, says doing this manually is prone to error.
Sure you could do everything without generics. But its a lot more error prone. Generics can in addition with other language construct provide better safety.
Generics also provide a tradeoff, that library code is more complicated, but its usage its easier to read.
But in general "systems programming" means "buffer processing" i.e. a lot of "streaming" and most buffers are short lived - they are never overwritten and you drop them soon after they were created - or they never change in size. (reallocation incurs unnecessary copies, and there are many good reasons to chunk large data sets up into fixed-width sizes). All this makes "stay in sync" very much a non-problem. I haven't had to worry about this in years. It's almost an academic exercise that only exists to prove that we need garbage collection ;-)
> Because the ever increasing number of CVE's where brilliant programmers who screw this up, says doing this manually is prone to error.
If you code in a memory unsafe language like C, things can go horribly wrong and your code can be exploited. If you code in a memory safe language, you can end up with horribly slow and unmaintainable code and you might still be exploited.
> Generics can in addition with other language construct provide better safety.
Better than using a hash table with generics, is not using a hash table at all...
Or, in practice, hiring somebody who actually understands how to program, understands what makes a library or language a good choice, and understands how to get the most value from both, without insane blinders.
What do you mean by "fixed data structures"?
> If you can provide a nice counterexample to the example that I gave
I may be missing somehing but I don't understand your example. The idea of a list is that we want it to hold any data, so the API to a list by necessity becomes generic. Unless you cas everything to void*.
---
I have another counterexample from userspace. Our internal APIs all use the same converntions which make working with them predictable and same from any language. An API returns:
{
result: [a list of data],
nextPageToken:
}
Different APIs will of course have different data. Some API will return a list of contracts. Another API will retur a list of media. A third API will... etc.I had the misfortune of working with these APIs in Go before generics. Welcome to copy-pasting code that deals with this. Or cast from interface{}/void* to proper data in runtime. Any sane language of course lets you write something like GetAllData<T>()
I don't know what you mean by "essential semantics", but the fact remains, as ncmncm says, that templates is the feature that allows new types of type safe generic abstractions to be built and added to the arsenal of C++ programmers. It's the feature that, most of all, has kept the C++ language alive and thriving over the past 20 years, in my opinion. Without it, the language would probably be dead.