What's Wrong with C++ Templates? (2003)
people.cs.uchicago.edu
people.cs.uchicago.edu
This claim needs some support. C++ has notoriously slow compile times, but note that he's talking about efficiency of the code generated, not the efficiency of code generation itself, which I agree is damaged by templates.
> It might surprise you to hear that, considering that C++ is "efficient" while functional languages like ML and Scheme are "inefficient," but it's true. C++ templates hide your intentions from the compiler: did you mean type abstraction? Did you mean a syntax extension? Did you mean constant-folding or compile-time code generation? The compiler doesn't know, so it has to just blindly apply the copy-and-paste strategy and then see what happens.
I'm going to propose that this isn't a bug, it's a feature. The optimizer should be completely independent of syntax - and that's what C++ templates are, automatic AST expanders (plus some compile-time computation). All that the optimizer sees in the end are basically unrolled assembly instructions or IR, and we have battle-tested techniques to optimize this. Most of the heavy-duty optimizations in modern industrial compilers happen on the linear IR representation anyway.
While templated code can be fatter than non-, link-time optimization can often reduce bloat when your your ad-hoc polymorphic template is really just acting as a parametric-polymorphic function a la ML.
Now that I've played devil's advocate, someone more familiar with GHC, please tell me how Haskell has optimizations (e.g. deforestation? or is that just a solution for a problem that is specific to Haskell in the first place?) that make my assertions above untrue :)
You're correct about the structure of the sentence, but note that people very commonly say something other than what they mean. Compare the rhetorical device of "transferred epithet": https://en.wikipedia.org/wiki/Hypallage
> a figure of speech in which the syntactic relationship between two terms is interchanged, or -- more frequently -- a modifier is syntactically linked to an item other than the one that it modifies semantically.
>> "While he's waiting, Richard pops a nervous handful of salted nuts into his mouth."
Who was nervous, the nuts or Richard?
I actually took the CRC32 algorithm and just slapped a template <int polynomial> and a constexpr on it, and called it CRC32 at compile-time. I'm using it for tons of string-hashing stuff where I want to replace a string with just a 32-bit number. It's incredibly efficient.
https://github.com/fwsGonzo/rvscript/blob/master/engine/scri...
IIRC you can store a null-terminated string as constexpr, right? If so, then there's your lookup table.
Circle is a pretty cool alternative for C++ metaprogramming: https://www.circle-lang.org it's just a prototype though, so who knows if it will ever become standard.
And in my opinion, legibility is an extremely important feature when you're writing code that you plan to understand and maintain for years.
Writting less code is a nice-to-have, but if the cost is that it's a complete mess, I'm out.
Hell breaks loose when you start doing "metaprogramming". I did a bit of it and it felt like I was exploited the compiler in clever ways not intended by the original authors, SFINAE checks in particular. And to be honest, I think it is the case, or some people should have their head checked.
From the look of it, some modern C++ features attempt to fix that mess, notably concepts in C++20.
Said another way: writing is ugly, and reading is inscrutable.
I'm not sure there's strong evidence (certainly none presented) about performance, etc
Truth be told, the syntax is not complicated. What's complicated is how templates are abused to do stuff that wasn't identified in the feature's initial design requirements.
Debugging is an implementation issue, not a language design issue.
Templates are awesome. STL's generic containers are a blessing to the point where they can be used in practically all applications without the developer giving a second thought.
I have very limited exposure to C++, so I'm not sure whether you're saying that some implementations have gotten this right. That isn't the impression I get as an outsider.
I recall a time where GCC's error messages were infuriatingly cryptic.
Then clang came up, and suddenly cryptic error messages were not only clear but helpful in identifying root causes.
If your line of thought made sense, C++ would have been an awful language until clang fixed it.
Don't confuse "implementation X didn't bothered to pay attention to a feature" with an issue in how a language was designed.
So the question is whether these template issues are found in every implementation (language design issue) or only some (implementation issue).
std::vector<std::pair<std::string, std::string>> vec;
anything involving nested namespaces and template parameters gets out of control really fast. the textual inclusion paradigm means I must declare the vector this way unless I want to inject everything from std:: into any source file that includes the header.I'm not sure what GP means about debugging. in my experience it's just like any other language if you have symbols and minimal optimizations. it can be kinda weird with optimizations, but that's to be expected. if the compiler can optimize away entire blocks of code, I don't think a debugger can abstract over that.
typedef std::pair<std::string, std::string> TStringPair;
std::vector<TStringPair> vec;
or maybe: typedef std::vector<std::pair<std::string, std::string>> TStringPairVec;
TStringPairVec vec;
this improves readability a bit, but is even more verbose at the declaration site if I'm not reusing the typedefs elsewhere. in an ideal world, I'd like something analogous to #undef for `using namespace` (or better yet, have this be the default at the end of a header file). this is only scratching the surface of the gotchas that come from textual inclusion.The gist of its workings is including a header and defining its type:
#define T int
#include <vec.h>
#define T float
#include <lst.h>
#define T str
#include <deq.h>
For more complex types, copy, free, and default constructors are supported, given they're defined before the inclusion of the container: typedef struct { void* data; } blob;
blob blob_copy(blob* self) { ... } // Copy constructor
void blob_free(blob* self) { ... } // Destructor
blob blob_init(void* data) { ... } // Constructor
blob blob_init_default(void) { ... } // Default constructor
#define T blob
#include <vec.h>
int main(void)
{
vec_blob blobs = vec_blob_init();
vec_bloc_push_back(&blobs, blob_init(...)); // Invokes constructor, element 0, passed by value
vec_blob_resize(&blobs, 42); // Invokes default constructor for elements 1-42.
vec_blob copy = vec_blob_copy(&blobs); // Invokes copy constructor for elements 0-42.
vec_blob_free(&blobs); // Invokes destructor for elements 0-42.
vec_blob_free(©);
}
The motivation for starting this project was to provide C++ like functionality to C, but provide hugely reduced build times.Unsurprisingly, what I found was that it was an uphill battle to replicate behaviour and performance that would be found under a better type system.
Things like "aggregate values matching this predicate with this folding lambda into this target container" were clumsy as hell.
But I like your approach of abusing preprocessor definitions prior to including the container header. Neat!
https://github.com/dleslie/dl/tree/master/include
https://github.com/dleslie/dl/blob/master/src/test_dl_algori...
At least with a bit of pre-processor magic, type safety and contiguous data is promised. Also, getting crafty with precompiled headers, one could dump all the expansions into a single templates.h, compile a .gch, and reap the benefits of hundreds of containered types with instantaneous compile times.
[1] https://github.com/nothings/stb/blob/master/stretchy_buffer....
I don't use either of these tricks much. Metaprogramming is especially bad, pretty much unreadable once written.
There's a third useful thing people do with templates - pass integer numbers to compiler. Unlike meta-programming, doesn't hurt readability.
There're instructions like _mm_shuffle_ps or _mm_srli_epi16 or _mm_extract_epi32 which encode an integer into machine codes. Instructions can encode memory references like `ptr [rax+32]`. And even without low-level trickery, it's sometimes useful to compile the same code multiple times, with changes somewhere. Templates is an easy way to express that in C++, here's an example: https://stackoverflow.com/a/59495197/126995. A nice related feature is `if constexpr` from C++/17, such branches are resolved at compile-time and therefore free.
The main problem with templates are the compiler error messages.
By contract, a std::vector<std::string> and a std::vector<int> in C++ are two completely different and unrelated classes. Any code they used is compiled twice, once for every value of the template parameter, and of course is optimized independently in each case. So, for example, although the template code is the same, the operation of copying a vector an give very different assembly codes: copying a vector of strings requires to call the copy constructor of each single string in the new vector, but copying a vector of ints basically reduces to calling memcpy() appropriately (thanks to all the appropriate abstractions, so that the compiler knows that to copy an int it is enough to copy its bytes, and this cannot fail due to an exception, and maybe other things).
This is one of the great thing in C++ (at least, if you value this kind of optimizations), and it seems that OP misses the point entirely. It gives what is called (when it works) "zero-cost abstraction". You might like it or not, but it makes no sense to judge C++ design choices if you don't consider the ability to do zero-cost abstraction (which, for example, Java does not have).