Kuro5hin.org: technology and culture, from the trenches
create account | help/FAQ | contact | links | search | IRC | site news
[ Everything | Diaries | Technology | Science | Culture | Politics | Media | News | Internet | Op-Ed | Fiction | Meta | MLP ]
We need your support: buy an ad | premium membership

[P]
What's wrong with C++ templates?

By jacob in Technology
Tue May 27, 2003 at 11:47:08 AM EST
Tags: Software (all tags)
Software

If you've read The Hitchhiker's Guide to the Galaxy and its sequels, you probably remember the Vogons, the incredibly ugly, disgusting, and bad-tempered aliens charged with destroying Earth to clear the path for an intergalactic highway. The Vogons' brains, it turns out, were "originally a badly deformed, misplaced and dyspeptic liver" -- and that explains their demeanor. In this article, I'll explain why I think C++ has a badly deformed, misplaced and dyspeptic liver of its own: its template system.


Before I make my case, I want to make sure my position on templates is clear. For the record: if you're going to program in C++, templates are unquestionably useful, and I hope you won't mistake me for one of those people who say that templates aren't necessary and we should all be using inheritance instead or some gobbledygook like that -- I'm not. If you want to program in C++ there are lots of times when templates are absolutely the best option the language gives you for writing generic, reusable code.

On the other hand, just because templates are the best C++ has to offer doesn't mean they're good. It turns out that all the problems templates solve were already solved better, before templates were ever introduced, by other languages. In fact, the kludgey way templates work precludes C++ programmers from a lot of the good stuff that nicer solutions have.

What are templates, and why should I care?

Templates are a widely-used C++ feature that have simple behavior with complicated consequences. What they do is allow you to mark particular code fragments that might otherwise look like functions, methods or classes as being, in fact, templates for those functions, methods or classes that have holes in them where later on, different people can place certain interesting constants, like type names or numbers. For example, the function


int myCleverFunction() {
return 4;
}

is just a regular function, but


template <int N>
int myCleverFunction() {
return N;
}

isn't a function at all, but a pattern for many different functions that the user can make just by supplying a concrete value for N.

Sound useless? It's not. There are a couple of very useful things people do with templates: one is writing code that is abstracted over types, and another is a clever trick called template metaprogramming in which programmers use templates to actually make decisions or perform calculations during compilation, sometimes with amazing effects.

In the rest of this article, we'll look at the various ways people use C++ templates and we'll see what features other languages provide to let programmers achieve the same effects. We'll look at basic and advanced topics, starting with the original and simplest use of templates: generics, also known as parametric polymorphism or just "writing the same code to work with multiple types of data."

Generics: write once, run anywhere

Most programmers who use templates use them to write data structures like lists and other containers. Templates are a natural match for lists (and container classes in general) because they not only let programmers write one List implementation rather than many different List kinds for all the different types of values that lists will need to store, but also let them write down statically-checkable rules like "this particular list must contain ints only."

For instance, In C++, you could write a simple linked-list as follows:


template <class T>
class ListNode {
public:
ListNode(T it, ListNode* next) {
this->it = it;
this->next = next;
}
T getItem() { return it; }
ListNode* nextNode() { return next; }
private:
T it;
ListNode* next;
};

When the compiler sees this code, it remembers the definition but emits no assembly instructions. Later, when it sees a use of the template instantiated with a particular type (say, int) that it hasn't seen before, it generates a fresh code fragment by replacing T with int everywhere in the body of the class definition and changing the class name to be unique, and then rewrites the usage to refer to the newly-generated code. So the code would allow you to write type-safe lists of any type:


// fine
ListNode<int>* il = new ListNode<int>(2,
new ListNode<int>(4,
NULL));
// also fine
ListNode<string>* sl = new ListNode<string>("hi",
new ListNode<string>("bye",
NULL));
// type error
ListNode<int>* il2 = new ListNode<int>(3,
new ListNode<string>("hi",
NULL));
// fine
int i = il->getItem();
// also fine
string s = sl->getItem();
// type error
string s2 = il->getItem();

This is a very handy trick, and one that you can't get any other way in C++ (even using void pointers or single-rooted class hierarchies, neither of which provide type-safety).

So handy, in fact, that it's hard to believe that nobody had thought of the idea before C++ templates were introduced in the mid-80's. You might know that C++ got the idea from Ada, but what you may not know is that the idea predates both -- in fact, the earliest versions of ML in the mid-seventies used a type-inference scheme that explicitly allowed functions to have polymorphic types. The notion had been around in research literature earlier than that, but ML was the first real programming language to have the feature.

ML's approach to the problem of writing a function that works on arbitrary types is very different from C++'s. In ML, the system isn't a pre-type-checking phase that generates new copies of the code for every different type of value the code gets used with, but instead it's a feature of ML's type-checker that allows it to make clever deductions about how functions behave. It tries to infer types for functions based on their source code: for instance, if the SML/NJ compiler sees the function definition

fun f(a,b) = a + 2*b

it is smart enough to realize that a and b must be numbers and the result type must also be a number -- even though the programmer didn't have to type that in, the type-checker realizes it anyway. On the other hand, if it sees

fun g(x) = x

it will conclude that x can be anything and the return type will be whatever was input. This is a perfectly sensible type, called a polymorphic type, and the type-checker can reason about it just fine. For example, if somewhere else in the same program it sees the code fragment

fun h(a,b) = g(f(a,b))

it will know that h takes two numbers and returns a number.

ML's type-checker gives ML programmers every bit as much power to write type-independent programs as C++ templates give C++ programmers: for example, we could write the SML/NJ version of the linked-list template above like so:


datatype 'a List = ListNode of 'a * 'a List | Empty
exception EmptyList

fun getItem (ListNode (i,_)) = i
| getItem (Empty) = raise EmptyList

fun nextNode (ListNode(_,rest)) = rest
| nextNode (Empty) = raise EmptyList

and the same lists will typecheck:


- val il = ListNode(2, ListNode(4, Empty));
val il = ListNode (2,ListNode (4,Empty)) : int List

- val sl = ListNode("hi", ListNode("bye", Empty));
val sl = ListNode ("hi",ListNode ("bye",Empty)) : string List

- val il2 = ListNode(3, ListNode("hi",Empty));
stdIn:3.1-3.36 Error: operator and operand don't agree [literal]
operator domain: int * int List
operand: int * string List
in expression:
ListNode (3,ListNode ("hi",Empty))

- val i = getItem(il);
val i = 2 : int

- val s = getItem(sl);
val s = "hi" : string

- val s2 : string = getItem(il);
stdIn:5.1-5.30 Error: pattern and expression in val dec don't agree [tycon misma
tch]
pattern: string
expression: int
in declaration:
s2 : string = getItem il

Aside from the syntactic differences and the fact that ML deduces types on its own, the C++ version and the SML/NJ version appear pretty similar. But the ML way offers a few tangible benefits: first, the ML compiler can check to ensure that a polymorphically-typed function has no type errors even if you never call it (C++ can't check your template for type errors until you instantiate it, and must check each instantiation separately), which is a big advantage for incremental development and for library authors. Furthermore, if ML says your polymorphic function is safe, then it's guaranteed to be safe no matter what types anybody uses with it: in C++, just because your template compiles with types A, B, and C doesn't say anything about whether it will compile if you instantiate it with type D later. This strategy also allows an ML compiler's code generator to make tradeoffs between the size of the code it generates and that code's efficiency -- C++ compilers get no such choice.

Interfaces: the virtue of (implementation) ignorance

As cool as ML's polymorphic functions are, you may have already realized that they have a pretty major drawback compared to templates: you can't rely on universally-quantified types to have any properties at all. That means that while you could write a function that took a list of any type and computed its length (because the length of a list doesn't depend on anything about the type of elements it holds), you couldn't write a function that sorted that list in any meaningful way without doing some extra work (because the proper sorting of a list depends on properties of the elements it holds).

You never have to worry about this problem when you write C++ templates. You just use any functions, methods, or operators you want, and when C++ fills in the template for you and recompiles the template body you'll automatically get the right thing (provided it exists, of course). For instance, you could add the following method to the C++ list example above with no problem:


template <class T>
class ListNode {
// ... as before ...
ostream & print(ostream &o) { return o << it; }<br> // ...
}

What happens when you apply this thing to a type? Well, if the type you used has an appropriate << operator defined for it, exactly what you'd expect happens, and print works fine. On the other hand, if it doesn't, you'll get an explosion of template error messages that don't really indicate the source of the problem. The worst thing about this situation is that it can cause insidious lurking bugs: the code works fine for a year, then one day the new guy uses it in a way that's not quite expected and all the sudden everything breaks for no obvious reason.

This is the sort of bug type systems were invented to catch, so it's not surprising that there are type systems that will catch it. The one you've most likely heard of is Java's interface system. That system allows a programmer to declare a certain bundle of method signatures apart from any class and then use that bundle as a type, meaning that methods can accept objects of any class that announces that it has all method for each method signature in the bundle.

This system works well, but unfortunately it requires that every class you want to use declares itself to implement a particular bundle of functionality ahead of time. ML's functor system (no relation to the things C++ calls functors, which in ML terms would simply be higher-order functions) deals with this problem nicely using the concept of a parameterized module system.

What's that? It's like a normal C++ library, but with some details (like types of things, for instance) sucked out so they have to be specified later. That may not make much sense, but hopefully an example will clarify: to add a print feature to the ML version of the list introduced above, we could rewrite it as a functor in the following way:


signature PRINTABLE = sig
type t
val printT : t -> unit
end

functor List(T : PRINTABLE) =
struct
datatype List = ListNode of T.t * List | Empty
exception EmptyList
(* ... other functions as before ... *)

fun print (ListNode (i,_)) = T.printT i
end

Now we can make new lists more-or-less on the fly, even holding types that don't have a printT function explicitly declared for them, like so:


- structure L = List(struct
type t = int
val printT = print o (Int.toString)
end);
structure L :
sig
val getItem : List -> T.t
val nextNode : List -> List
val print : List -> unit
exception EmptyList
datatype List = Empty | ListNode of T.t * List
end

- L.print (L.ListNode (5, L.ListNode(6, L.Empty)));
5
val it = () : unit

This system gives you more abstraction power than C++ templates or Java interfaces while providing type-safety.

Metaprogramming: the art of good timing

Another purpose for which particularly devious programmers can use C++ templates is "template metaprogramming," which means writing pieces of code that run while the main program gets compiled rather than when it runs. Here's an example of a program that computes the factorials of 4, 5, 6, and 7 (which are 24, 120, 720, and 5040) at compile-time:


#include <stdio.h>

template <int n>
class Fact {
public:
static const int val = Fact<n-1>::val * n;
};

class Fact<0> { public: static const int val = 1; };

int main() {
printf("fact 4 = %d\n", Fact<4>::val);
printf("fact 5 = %d\n", Fact<5>::val);
printf("fact 6 = %d\n", Fact<6>::val);
printf("fact 7 = %d\n", Fact<7>::val);

return 0;
}

If you look at the assembly code g++ or any other reasonable compiler produces for this code, you'll see that the compiler has inserted 24, 120, 720, and 5040 as immediate values in the arguments to printf, so there's absolutely no runtime cost to the computation. (I really encourage you to do this if you never have before: save the code as template.cc and compile with g++ -S template.cc. Now template.s is assembly code you can look over.) As the example suggests, it turns out that you can get the compiler to solve any problem a Turing machine can solve by means of template metaprogramming.

This technique might sound like some strange abuse of C++ that's primarily useful for code obfuscation, but it turns out to have some practical applications. For one thing, you can improve the speed of your programs by doing extra work in the compile phases, as the example shows. In addition to that, it turns out that you can actually use the same technique to provide convenient syntax for complicated operations while allowing them to achieve high performance (matrix-manipulation libraries, for instance, can be written using templates). If you're clever, you can even get effects like changing the order in which C++ evaluates expressions for particular chunks of code to produce closures or lazy evaluation.

Again, it turns out that this ability was old before templates were a glimmer in Bjarne Stroustrup's eye in the form of Lisp macros. You may recoil at the use of that name, but don't worry: Lisp macros are much more pleasant to work with than their higher-profile cousins. At about the same time Kernigan and Ritchie were inventing C and C preprocessor macros, a group at the MIT Media Lab was inventing a system called MacLISP that introduced Lisp macros, a totally different implementation of the macro concept that survives to this day in Common Lisp and Scheme as well as in a number of offshoots and related languages.

As they exist today, Lisp macros and C macros do similar things: they allow the programmer to substitute one fragment of code with another before the program gets run. The big difference between the two is that while C macros work by scanning for and replacing literal text phrases within source code, Lisp macros replace portions of a parse-tree instead. That might not sound revolutionary, but it turns out to be the difference between a system that gurus recommend you never use and one that goes a long way towards defining a language.

Lisp macros offer a kind of compile-time computation that goes one step above C++ template metaprogramming by allowing you to actually write your compile-time programs in Lisp itself. The same code you'd use to write a regular program, put in the proper place, runs at compile-time instead, and its result gets inserted into the source code of your program. For instance, here's how you could write a normal factorial function in Scheme (this is PLT Scheme version 203):


(define (fact n)
(cond
[(= n 0) 1]
[else (* n (fact (- n 1)))]))

If you wanted to make a version of the same function that was guaranteed to run at compile-time, you could just write:


(define-syntax (ctfact stx)
(define (fact n)
(cond
[(= n 0) 1]
[else (* n (fact (- n 1)))]))

(syntax-case stx ()
[(_ n) (fact (syntax-object->datum n))]))

Aside from some mumbo-jumbo telling Scheme that this is a macro and how to read its arguments, it's exactly the same as the original Scheme function. That's true in general of Lisp macros: they're just regular functions that you tell the language to run at compile-time rather than runtime. While that may not sound all that important, it actually makes huge practical difference: it allows your macros to use parts of your code that also run at runtime, to load libraries and make library calls at compile time -- in PLT Scheme, you could even write a macro that popped up a GUI with a dialog box that asked the user how to compile a particular expression! -- and more, all with no extra effort. C++ templates can't use normal run-time C++ code in the process of expanding, and suffer for it: for instance, the C++ factorial program is limited in that it produces 32-bit integers rather than arbitrary-length bignums. If we had that problem in a Lisp system, it would be no problem: just load a bignum package and rewrite your macro to use it, and everything works out (and still all happens at compile-time). In C++, though, the bignum library is no use to us at all, and we'd have to implement another "compile-time bignum" library to make the fix.

Just as metaprogramming is more powerful than computing little mathematical functions at runtime, Lisp macros have quite a few more uses too. In fact, they were made specifically for extending Lisp's syntax with new constructs. For instance, PLT Scheme has no equivalent of C++'s while loop, but you can add one in just a few lines of code:


(define-syntax (while stx)
(syntax-case stx ()
[(_ test body)
#'(letrec ((loop (lambda () (if test (begin body (loop))))))
(loop))]
[else (raise-syntax-error 'while "Illegal syntax in while loop" stx)]))

Notice in that code fragment how obvious it is what's happening, even if you don't know Scheme: whenever Scheme sees (while <test> <body>) for any code fragments test and body, it should replace that bit with


(letrec ((loop (lambda () (if test (begin body (loop))))))
(loop))
which is Scheme code that performs the loop properly. Otherwise, the user used bad syntax so print a syntax error.

Even with a simple example like while, you can begin to see how these macros are more powerful than C++ templates: while it's clear that since they perform the same copy-and-paste function that templates perform, they can fill the same role, they also have a lot more built-in support for making your metaprograms play well with your normal program. Allowing user-defined syntax errors, for example, would have been an easy way to let the STL authors write code that produced helpful, meaningful error messages rather than the notoriously unhelpful error messages it prints now.

In fact whole large syntax systems can easily be built out of this mechanism, particularly when you remember you can transform your syntax trees not just using pattern matching, but in fact using any arbitrary code you want. A good example of a big system you can build using macros is PLT Scheme's object-oriented programming system: it's a normal, unprivileged library that adds seamless object-oriented programming to PLT Scheme, which has no built-in object-oriented features. You get syntax forms, natural error messages, and everything else an in-language system would provide. In the Lisp world this is standard and many large-scale Lisp and Scheme projects use macros -- a quick check of the standard libraries included with the PLT Scheme distribution shows 292 uses of define-syntax in about 200,000 lines of Scheme. What's more amazing is that this count doesn't include the many macros that PLT Scheme uses to actually define the core of Scheme, like cond, define, let, and so on, which are all macros in PLT Scheme. It might surprise you to learn that in fact the only syntax forms in any of the PLT Scheme examples in this article that are not in fact macros that expand into some simpler form are the begin and if forms I used to implement the while loop above.

So what?

So some other languages invented some features before C++, and they implemented them in arguably better ways. So what?

For one thing, templates hurt the C++ compiler's ability to generate efficient code. 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. In ML or Scheme, though, aside from the individual benefits described above, the simple fact that you're telling the compiler what you want to achieve lets it optimize for you much more effectively.

Another reason to care is that if you understand the context in which templates exist, you'll be able to make more effective use of them and you'll be able to make more intelligent decisions about when to use them.

But from a broader perspective, realizing that templates are really just a C++ version of Lisp macros geared towards generating type declarations rather than extending C++'s syntax helps you understand the wider history of programming languages rather than just knowing the flavor of the month (which is rapidly becoming the flavor of last month!).

Sponsors

Voxel dot net
o Managed Hosting
o VoxCAST Content Delivery
o Raw Infrastructure

Login

Poll
What's the handiest feature of C++ templates?
o Generics 37%
o Interfaces 2%
o Metaprogramming 7%
o Other 2%
o I program in C++ but don't ever use templates 16%
o I don't ever program in C++ 33%

Votes: 95
Results | Other Polls

Related Links
o SML/NJ
o MacLISP
o Common Lisp
o Scheme
o PLT Scheme
o Also by jacob


Display: Sort:
What's wrong with C++ templates? | 324 comments (253 topical, 71 editorial, 0 hidden)
What's cool about templates (3.00 / 1) (#5)
by hobbified on Mon May 26, 2003 at 10:35:29 PM EST

I can't find it right now, but there was a post fairly recently on the perl6-language mailing list all about what's so cool about C++ templates and what's not quite right about them. If I get some time I'll find it, but for now I'll give you a chance to display some initiative, as I'm ready to get to sleep. :)


what's wrong with geeks? (1.04 / 45) (#13)
by BankofNigeria ATM on Mon May 26, 2003 at 11:22:33 PM EST

you elitist jerks, what are you doing masturbating to templates? there are people starving in this world.

1. S 2. V 3. PREP 4. V 5. N 6. PRO 7. N 8. PREP 9. V 10. V 11. V 12. PRO 13. PRO 14. V 15. N 16. V 17. PREP 18. ADV 19. N 20. ADV

hmm (1.25 / 4) (#14)
by tang gnat on Mon May 26, 2003 at 11:25:40 PM EST

Yes, I believe that's the approach Windows NT is taking.

Good article, bad thesis? and comments (4.75 / 4) (#22)
by sesh on Tue May 27, 2003 at 12:41:40 AM EST

Unfortunately my understanding of lisp and ML are so minimal that I cannot assess your comparison of the languages, but I do have a few questions/comments.

  • Do the mechanisms you mentioned extend to the OO paradigm (eg, class templates)?
  • Your examples all seem algorithm based - do they extend to concepts like policies and traits?
  • I cant see how you can avoid the mentioned optimisation problems when implementing templates if they are to remain completely generic. Also, do you have any benchmarking references indicating the benefits of ML generics over C++?
What is your point? Are you suggesting that C++ should use the same implementation mechanics and syntax as these other languages? Is it valid to compare major features of two languages with completely different architectures and purposes in such a small article?

I enjoyed the article, by the way, but I think your thesis didnt represent your article - your article was for far too narrow a scope (or perhaps it was the lack of examples), and the proposed thesis was for a very broad subject (templates are a major feature in C++).

Also, I think your attitude towards C++ templates will unfortunately bring the rabid knee-jerk C++ critics out of the woodworks.

What's wrong with bashing of C++ templates? (4.77 / 9) (#29)
by i on Tue May 27, 2003 at 01:57:34 AM EST

Let me concede one point right now: not everything. It is true that error reporting is abysmal. It is true that a mechanism akin of SML functors is badly needed. It is true that the syntax leaves much to desire.

So what's wrong with it?

Let us recall what C++ templates are. They are a Turing complete macro system geared toward types. That is, they eat typed code fragments (types or typed expressions) and produce typed code fragments (declarations, types or expressions).

Now this is true that other languages have better implementations of similar features. Languages of ML family have polymorphism, which can be thought of as a macro system geared toward types. Lisp and relatives have their own highly sophisticated Turing complete macro systems. What's so special about C++ templates?

Precisely their being both Turing complete and geared toward types. This makes them unique. No other mainstream language has such a combination of features. (Some experimental languages provide dependent types, a feature more powerful and complete than C++ templates, ML-style polymorphism and probably even Lisp macros.)

And it is precisely this combination of features that makes C++ templates suitable for generating efficient, sophisticated, type safe code, such as found in libraries like Blitz++ or SIunits.

Finally I would like to address the question of efficiency. Quoth the author:

For one thing, templates hurt the C++ compiler's ability to generate efficient code. 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. In ML or Scheme, though, aside from the individual benefits described above, the simple fact that you're telling the compiler what you want to achieve lets it optimize for you much more effectively.

Now this doesn't strike me as particularly convincing. Templates just generate perfectly ordinary C++ code. It is not entirely clear why such code should present less opportunities for optimisation than type-abstracted ML code. The only plausible reason is that typically there is more generated C++ code than generated ML code. Yes, the infamous template bloat. However, with a combination of reasonable compilation strategy and some help from the programmer this, too, can be mitigated.

One day I just might write an article about all this.

and we have a contradicton according to our assumptions and the factor theorem

From a recent email (2.00 / 2) (#33)
by KWillets on Tue May 27, 2003 at 02:18:31 AM EST

i.e. a friend operator taking a template class object as a parameter. In the compilers from a couple of years back (at least Watcom's), this used to work; with recent gcc and MS Visual C++ versions it doesn't.

This type of problem seems to come up all the time. I worked on a heavy C++ project a few years ago, and we skipped templates because of compiler differences.

The explanation of the principles of compile-time code execution makes a lot of sense. Most explanations of templates spend a lot of time on how versatile they are, without explaining the more theoretical issues and limitations.

Templates are not quite turing complete. (4.33 / 3) (#35)
by President Saddam on Tue May 27, 2003 at 02:43:12 AM EST

ANSI C++ only requires that compiles are able to nest templates to 17 levels (why 17? ask ANSI).

So your example of computing factorials can only be guaranteed to work up to 17 factorial, and the same can be said of any similar recursive/iterative metaprogramming performed with templates.

<editorial>
I'll abstain; this article isn't quite good enough.

Arguing against templates on 'efficiency' grounds  is meaningless. Do you mean speed or code size?. Templates are fast, but they do cause bloat.

As other people have mentioned their achille's heel is the fact that they increase code size considerably. However, partial specialisation should help that, but in practice I haven't seen huge gains. Also template members are only instantiated if they are used. Often, it seems that using the STL gives your code an extra megabyte.

---
Allah Akbar

-1 language wars (4.66 / 6) (#43)
by hugues on Tue May 27, 2003 at 03:05:17 AM EST

ML and Lisp are great languages as you demonstrate, there are others too: haskell, ocaml, etc.

I think that most practitioners of C++ are well aware of its hackish, complex and somewhat ugly nature. To me however it looks like a oboe: very complex, temperamental and hard to play, but beautiful in its complexity and unique.

Eventually most practitioners of C++ get to appreciate its good sides: the huge support across the industry, the fine, standard libraries (STL anyone?), the sheer speed. Your hand wavy argument about optimization are less than convincing. Low-level optimization does at least 90% of the work. There is not much that high-level optimization can do that I rethink of the algorithm can't. Let's see some real life benchmark, shall we?

Language advocacy is fine but there is nothing in your piece that can't be found anywhere else, and it's not short or to the point.

You've Argued Your Point Well (4.27 / 11) (#50)
by OldCoder on Tue May 27, 2003 at 04:28:22 AM EST

C++ is a research language that has gone mainstream. If C++ had been designed with the knowledge that was available in, say 1980, when it was first dribbling out of AT&T Bell Labs, it would be a better language than it is.

A basic historical flaw in C++ is that it was designed based on a flawed concept of compatibility with C. The good idea of compatibility was that C programmers could readily adopt to it and use it as "A better C". The mistake was deciding that C++ could compile and execute all the standard C programs (like all the code in K&R), as if C compilers would no longer be availabe after the switch.

C++ has too many features that were added to solve "Local problems" that were basically caused by the language. Templates are one example, invented to support container development. Then, too much cleverness was applied and the feature over-used to do things it was never intended to do.

Likewise, the hack of using << and>> as I/O operators was extemely clever but ran into the problem of needing friend functions that aren't in the inheritance tree. Come to think of it, friend functions are another hack to solve problems cause by the C++ language.

Another clever innovation that never quite worked out right is Smart Pointers. To get it right, the committee has decided to alter the next release of the language.

C++ has proven to be a decent place for some programming language experimentation and innovation but shows it's research roots. You tell from the design of C# or Java for example, that more time and effort and thought went into the language design with regard to the community of programmers who would use the language. C++ was designed by a very small group that didn't have a large support staff to work out bugs. The small staff couldn't consider programming language alternatives from the perspective of knowing a great many other languages well.

The same critique of solving a language problem rather than a customer problem applies to the "Curiously recurring template patterns" where a base class has its derived class as a template parameter. The base class is only instantiated when defining the derived class. This is an extremely clever trick that solves problems. Unfortunately, the problems it solves are the problems created by the C++ language, and not the end user problem the programmer is supposed to be working on.

Even the much praised "Traits" feature is an off the cuff too-clever invention that was created to solve problems with the C++ programming language rather than the problems of the end-user.

The book C++ Gems is a cornucopia of solutions to programming-language problems pretending to be programming solutions. The authors are very proud of their inventions.

Using cleverness to trick a compiler or a programming language produces code that is hard to understand and maintain. This all reminds me of early FORTRAN; The users of the FORTRAN feature called "Equivalence classes" used a lot of cleverness to produce brilliant kludges to solve problems that shouldn't even exist. Smart people wrote mathematical papers on the theory to use in a compiler so the compiler could figure out how to implement Equivalence classes efficiently. The whole topic is justifiably gone from programming.

Having said all that, I must be a little fair and vent about C# and Java: The choice of programming languages shouldn't also fix your choice of whether to use Garbage Collection, C+ destructors, COM release semantics or whatever. Languages should be flexible enough in this day and age to permit some choice. Only C++ even tries.

The premature release of OOP upon the innocent world of programming was sold as the way to achieve "Code Reuse" and "Productivity". It is only recently that the problems introduced by the new programming paradigm have begun to be solved in convincing ways. Take a look at the C# keywords override and new as applied to class declarations for a nice solution to a problem that vexed C++ for at least a decade (foreign base class upgrades that define methods synonomous with existing methods in derived domestic derived classes).

After all these years, there is still no silver bullet.

--
By reading this signature, you have agreed.
Copyright © 2003 OldCoder

Taking the Vogon analogy further (4.75 / 4) (#54)
by arvindn on Tue May 27, 2003 at 04:39:12 AM EST

It turns out that all the problems templates solve were already solved better, before templates were ever introduced, by other languages.

Curiuosly, this has a close parallel in the Hitchhiker's guide as well. It turns out that all the problems that the destruction of the earth solved were already solved better, before the Vogons ever reached the earth, using other technologies. Indeed,

"...a wonderful new form of spaceship drive was at this moment being unveiled at a government research base on Damogran which would henceforth make all hyperspatial express routes unnecessary." (Chapter 5).

So you think your vocabulary's good?

Bah. C++ itself (2.55 / 9) (#61)
by porkchop_d_clown on Tue May 27, 2003 at 08:01:24 AM EST

Is a bastard language, gluing OO onto what was intended to be a highly efficient, low-level language.

If you want OO, use something that was designed for OO from the ground up. If you want efficiency, use C. If you want to suffer, use C++.


--
I only read Usenet for the articles.


Objective-C (3.00 / 1) (#74)
by Random Number Generator Troll on Tue May 27, 2003 at 09:01:38 AM EST

I have a friend at work who knows C++ very well, and is interested in learning ObjC. I spent about 3 days trying to learn C++ but I got distracted and never got very far, however I know ObjC quite well now. But this friend keeps asking wether ObjC deals with templates. Before I read this article I didn't have a clue because I didn't know what templates where, but now I think I can hazard a guess...

I guess that because I can just pass in, and return id to a function (id is a pointer to an object of unspecified type), and then just say [incomingObject someAction], there is no need for templates? If not, how does ObjC go about solving the templates problem?

C++ Articles (3.66 / 3) (#90)
by CaptainSuperBoy on Tue May 27, 2003 at 11:06:49 AM EST

Every successive "What's wrong with C++" article makes me a little more confident in my decision never to learn C++. Luckily I have never needed to use the language in the past, and all my code nowadays is pretty much in a high level language. I'm a firm believer that unless there is an honest need to do it low level, high level and/or RAD is probably better.

--
jimmysquid.com - I take pictures.
Haskell classes (5.00 / 2) (#96)
by carlossch on Tue May 27, 2003 at 12:11:02 PM EST

I liked the article, but I think you could have mentioned Haskell classes, specially when you talk about the problems with universally quantified types and needing to know properties of those types. The Haskell type system allows just that. You define a class:

class Eq a where
  equal :: a -> a -> Bool

and then define instances of that class for each type in which you want to let the system know about the properties. For example:

instance Eq Int where
  equal :: Int -> Int -> Bool
  equal 0 0 = True
  equal 1 1 = True
...
  equal x y = False

(Obviously the real function does not work by enumarting all equal pairs.) Having defined a class, a function can use the operations this class provides by annotating the type accordingly:

sort :: Eq a => [a] -> [a]
(sort uses 'equal' at some point)

By the way, in all but some hairy cases, this annotation can be inferred by the type-checker.

There are many other nifty things you can do with Haskell's type system, but its original purpose is to solve exactly the sort of problem you describe up there.

Nice article, nevertheless.

Carlos
He took a duck in the face at two hundred and fifty miles an hour.

Hard to read (4.66 / 6) (#105)
by egg troll on Tue May 27, 2003 at 01:37:54 PM EST

Is it just me, or is anything written with the code tag really hard to read. Rusty, why oh why did you elect to make said tag use a five-point font?!

He's a bondage fan, a gastronome, a sensualist
Unparalleled for sinister lasciviousness.

Template bloat (4.50 / 2) (#119)
by lauraw on Tue May 27, 2003 at 02:15:22 PM EST

[I posted this as an editorial comment last night, but I decided to re-post most of it now that the article has been voted up....]

I'm a fan of template when they're used appropriately, but I don't think this article has enough emphasis on one of the major problems of C++ templates: "bloat" from template code generation. This can be a huge problem in large, complex programs with lots of instantiations.

This is a hot-button for me because I worked at Taligent, the company that was trying to build a new, object-oriented operating system in C++. It could be argued that templates were the main reason that Taligent didn't succeed. (It could also be argued that nobody really wanted an overdesigned, object-oriented operating system. :-) After the Taligent system had templates inflicted on it, well over 1/2 of the object code in the system was the result of template instantiations, and it caused a huge performance degradation and ridiculous resource requirements. We ended up inventing all sorts of ugly hacks to get rid of template code. Yuck.

The main reason for the bloat was that the C++ template system was basically designed as a glorified macro preprocessor. It would "paste" in the type-specific code for a particular instantiation when it was needed. A good contrast is the new "generics" that will be in Java 1.5. They designed it in such a way that new code isn't generated for each instantiation, which is a big improvement. Of course, this is much easier to do in Java because of all the run-time type information that the VM has available. And the downside is that you can't use generics on primitive types.

Still, C++ templates can be very useful in some circumstances. One of the techniques I used a lot was to create a templatized wrapper around a non-typesafe implementation class, which minimizes bloat and still gives you type safety. But my C++ is now rusty; I've been doing Java for the last few years.

-- Laura

Couple of questions (none / 0) (#121)
by yamla on Tue May 27, 2003 at 02:18:40 PM EST

I have two questions.  I'm not claiming that templates are wonderful, just clearing up a couple of possible misunderstandings I have about your article.

First, you claim that all the problems solved by templates were solved better by other languages (without using templates).  You use the example of ML that allowed polymorphic types without templates.  Is ML's type-inference scheme done at compile time or at runtime?  I'm assuming compile-time because templates are a compile-time feature of C++.

Second, the interfaces problem.  This is indeed problematic in C++ with templates.  However, it seems to me that there is nothing stopping you from defining an interface class and inheriting from that.  You can then use templates (specifically, partial template specialisation and template metaprogramming) to ensure your templated functions only operate on classes that inherit from your interface class, thus guaranteeing your templated functions are never used on types that don't fit the bill.  You may be able to do this even without defining an interface, my metaprogramming skills aren't up to the task, however.  Certainly, C++ doesn't require you to use templates like this but doesn't the fact that you can diminish your argument somewhat?

Meta-programming (5.00 / 2) (#129)
by The Writer on Tue May 27, 2003 at 02:37:57 PM EST

I've been fascinated with metaprogramming for a while now. Although C++ templates may not be the best place for this (though you can argue both ways), I think metaprogramming should be a fundamental part of modern programming languages.

I've often written programs which are driven by tables, or other such data, which is constructed at compile-time rather than runtime. A common example is parsers and lexers. The reason tools like flex and bison exist is because it is very tedious to hand-code these tables. You want a higher-level representation (eg., grammar rules and lexical regexes) that a human can read and modify easily; you don't want to have to recompute state changes and rule numbers every time you tweak something in your grammar.

Other examples include game rules and customizations which are built into a game engine at compile-time: you don't want to wait to runtime to compute these things, 'cos it's unnecessary overhead. Especially if you have scriptable features. Or, you have a set of optional modules which you can compile into your webserver core; but you need to hand-tweak various module tables and lists before you can do it, or sprinkle tons of #ifdef's all over the code to make it work.

Meta-programming makes these problems much more manageable. For example, some of my programs have perl scripts that run at build-time, to generate from specification files large tables that would be very time-consuming and error-prone if done by hand. One such program has a flex input file that contains over 200 tokens, each returning a bitfield-encoded token number. The rules are extremely repetitive (they are essentially all permutations of a set of symbols), but the program is very sensitive to incorrect token numbers. Coding this by hand is ridiculously tedious, and can easily introduce errors that are hard to detect. The solution? A spec file that describes the set of symbols to permute, the rules for computing the token number, and a Perl script to transform this into a flex input file at compile-time. Essentially, the spec file is a kind of "meta-language" that describes the program at a higher level; the Perl script then translates this into a lower-level format that the compile tools understand.

This is all fine and dandy, except for one flaw: using Perl scripts, or other external means of meta-programming, is outside the domain of the compiler; so a lot of compiler features like type-checking, etc., are not directly accessible. This means you can get type errors because the tool you use failed to check for some boundary condition. Just like in C++ templates, the errors are very cryptic because they come from a different layer than the high-level meta-source that you're working with.

It would be great if compilers (or languages for that matter) can be built with meta-programming support in mind. It isn't really that hard: you can just have a meta section in the same programming language (or a suitable subset thereof), which is executed at compile time by the compiler, to produce (all or part of) the program text to be compiled. The compiler can then perform full syntax and type-checking for you.

My problems with C++ templates (5.00 / 2) (#132)
by bhurt on Tue May 27, 2003 at 03:23:30 PM EST

I have two problems with templates as they're implemented in C++:

1) They break LALR(1) parsing of the language.  Consider the sequence of tokens:
    a < b , c > d ;
In C, how to parse this expression is clear: it's two comparisons joined by the comma operator, and is parsed like:
    ( a < b ) , ( c > d ) ;
Slightly odd, but perfectly legal and understandable.  C++ has that possible implementation, but in addition that above sequence of symbols could also be a variable declaration, introducing variable d with a type a<b,c>.  You can't parse C++ with classic compiler tools like Lex and Yacc.

This is a minor nit in some ways, but displays (I think) a larger flaw in C++ in general- that it was standardized without first being implemented.  An implementation before the template syntax was standardized would have brought up the LALR(1) parsing problem- which could then be easily solved simply by changing the < > to some other symbol combination- say, <[ ]>.

2) Code generation.  In general, when ML or Ocaml or the like generate a function with an abstract type, it only needs to generate one version of that function.  In Java terms- everything, even ints and booleans, are full objects, derived from class Object (not exactly, but close enough to get the idea across).  So the abstract function, at the assembly code level, only needs to handle Objects (or void*'s for C programmers).  To prevent this from being a performance problem, the compiler often "unboxes" small data types like ints and booleans.

With C++ every instantiation of the template with different arguments requires a different instantiation of the code.  If you have a template foo<>, foo<int> is one implementation, foo<char> another, foo<double> a third, and so on.  All of a sudden 2K of code becomes 20K, or even 200K (as the compiler finds it hard to know that it only needs one implementation for foo<bar*> and foo<baz*>).  Not to mention how many times I've seen stupid template definitions- like the array template which took not only the type of objects to hold, but also the number of items to hold.  So arrays of length 3 had a different implementation than arrays of length 4.  

This code specialization comes at a cost- a cost of larger executables, larger memory foot prints, and larger code working sets making cache less usefull.

Brian

Use 'em all the time, couldn't live without 'em. (5.00 / 2) (#139)
by alyosha1 on Tue May 27, 2003 at 04:16:08 PM EST

I'm in the process of implementing a medical imaging library , that would have been vastly more complicated without the powerful features of C++ templates. The library implements the DICOM (Digital Imaging and Communications In Medicine) standard, which is a huge document several thousand pages long. Dicom has it's own type scheme declaring types such as 'UL' (unsigned long), 'PN' (Patient name) and so on.

To interface with dicom images in C++ their needs to be a mapping between dicom types and c++ types. (Similar issues arise in other problem domains - any database application needs to map SQL data types to C++ types).

Templates allow me to state these relationships explicitly and simply:

typedef CppTypeFromDicomType<PN>::Type CppType ;
CppType data;
cout << data;

In the above (simplified) snippet, 'PN' is an enum value specified in the dicom standard, and CppTypeFromDicomType is a template meta-function that at compile time looks up the enum value 'PN' and tells the compiler (in this case) "the variable 'data' has type 'string'". If we'd fed in the enum value 'UL', then 'data' would have type 'unsigned long', and so on.

I haven't explained how the meta-function works here but you can see it in the VR.hpp file in dicomlib.zip on the linked site.

Since I implemented this, I have not run into ANY type-mismatch bugs or problems in my library between dicom types and c++ types. Because this all happens at compile time, the code will refuse to compile if I make a type error - for example, trying to write a C++ integer onto a DICOM string.

Another place where I've seen something similar used is in the very excellent libpqxx C++ client API for postgresql. A value returned from an SQL query can obviously have any SQL data type, and libpqxx makes it very easy to extract that value onto a C++ type. Here's a snippet from one of my applications that use the library:

void SomeFunction(const Result::Tuple& Row)
{
string Host;int Port;
Row["host"].to(Host);
Row["port"].to(Port);
}


Note how libpqxx automagically figures out what type I want and handles all the data type conversion, using some clever behind-the-scenes template technique that I, the end programmer, don't even need to worry about.

So no, templates are not perfect, the syntax can be ugly at times and the learning curve steep, but here's one programmer who finds them indespensible for churning out robust, usable library code.

Why C++ truly sucks. (1.33 / 3) (#140)
by Phillip Asheo on Tue May 27, 2003 at 04:35:26 PM EST

Because people try and use it for the WRONG things...

They use it where they should use Java, they use it where they should use Pearl, hell they use it where they should use C!

If I had a dollar for every line of shit C++ code I have had to read during my employment, I would have a lot of cash by now.

C++ is a truly shitty language. However, if people want to pay me $$$s to clean up their 'mission critical' shitty C++ mess after them, thats fine by me.

--
"Never say what you can grunt. Never grunt what you can wink. Never wink what you can nod, never nod what you can shrug, and don't shrug when it ain't necessary"
-Earl Long

Templates are only a big deal to C++ newbies (5.00 / 2) (#142)
by DodgyGeezer on Tue May 27, 2003 at 04:46:42 PM EST

"On the other hand, if it doesn't, you'll get an explosion of template error messages that don't really indicate the source of the problem. The worst thing about this situation is that it can cause insidious lurking bugs: the code works fine for a year, then one day the new guy uses it in a way that's not quite expected and all the sudden everything breaks for no obvious reason."

If you get errors, it hide away for a year as it won't compile.  If it's a bunch of warnings and they're ignored, the programmer is either bad or inexperienced.  The "explosion of template error messages" isn't such a big deal for somebody who isn't new to the language.  Experience and commonsense track them down... and if you're writing new code, then you know where it probably lies already.

Unrelated to templates, but as for the point about Java interfaces: this has always been possible in C++ through the use of pure virtual classes.  It's not quite as neat as Java and can easily be changed without warning by somebody later on, but it's still there.  It's used extremely heavily by implementations of things like COM.

It seems most people who have a problem with templates really have a problem with writing them.  I can understand that... but then I rarely have to write new templates.  Instead I VERY HEAVILY use the STL - with that the template syntax is only evident during variable declaration and compilation errors.  Debugging in to the STL is an endeavour of patience and experience, but that's nothing to do with templates, just the very terse code that would be hard to follow even if it wasn't parametric.

As good as it gets (4.00 / 2) (#145)
by glauber on Tue May 27, 2003 at 05:01:41 PM EST

Templates are not wrong. In fact, they're as good as you can get with a C/C++ type of language. Granted, you can do better in ML and in Lisp, but C++ isn't ML and it isn't Lisp either.

You can see the need that C++ templates fill as soon as you start programming in Java. Java is supposed to be safer than C because it's type safe, but as soon as you start using its collections framework, Java is no better than C, because you have to cast everything. C++ solves this elegantly through templates.

On the other hand, use of templates for pre-compiling optimization is a hack that should be punished by death through unending debugging sessions.


glauber (PGP Key: 0x44CFAA9B)

Polymorphism doesn't do it (none / 0) (#150)
by glauber on Tue May 27, 2003 at 05:16:45 PM EST

You can't implement a generic collections framework using polymorphism. Or rather, you can, but it's meaningless. In Java, all Lists are lists of "Object", the root class of Java's hierarchy. These "Object"s are almost useless until you cast them to something else higher up that tree.


glauber (PGP Key: 0x44CFAA9B)

Good article. (4.00 / 3) (#167)
by reflective recursion on Tue May 27, 2003 at 06:29:44 PM EST

I have only briefly used C++ and have never gotten as far as using templates. Interesting stuff.

Lisp/Scheme vs. C++ macros/templates...
The big difference between the two is that while C macros work by scanning for and replacing literal text phrases within source code, Lisp macros replace portions of a parse-tree instead. That might not sound revolutionary, but it turns out to be the difference between a system that gurus recommend you never use and one that goes a long way towards defining a language.
If I'm reading that right, you're saying C++ gurus recommend staying away from templates. The funny thing is that most Lisp gurus say the same about their macros. Macros as specified in Common Lisp standard can be extremely dangerous to use because they can inadvertently capture variables that exist outside the macro definition. The only remedy is using gensym style functions. While that may seem hackish, it is quite a bit nicer IMO than what Scheme does.

Scheme has what are called "hygienic" macros, which are designed to prevent that inadvertent variable capture of Common Lisp style macros (defmacro and cousins). While they are okay for "typical" macros, they do pose a serious problem with defining macros which create definitions themselves (such as creating object-oriented structures or C-style structs/records). Scheme macros are also very ill-specified and leave a couple of semantic ambiguous situations. And then you have the situation that they are a complete bitch to implement "correctly" (depending on how you interpret the R5RS spec), whereas CL style macros are a simple quasiquote transformation with a repeated evaluation. Which is why most Scheme implementations will usually implement CL-style macros, if they implement a macro system at all.

I don't have much to say about your conclusion. Except, perhaps, I hope I'm never in a situation where I have to use them (or C++ for that matter).

What about implementation-hiding? (none / 0) (#172)
by coderlemming on Tue May 27, 2003 at 06:50:49 PM EST

It's interesting that, in this article and the entire set of replies so far, I haven't seen this important problem with templates:

What if I want to write a library with classes that are not type-specific, and I want to sell this library in a closed-source form? It's not possible. In C++ sans templates, I can just pass out the .h files to provide the public interface, and distribute the compiled libraries. But C++ requires that the entire template class is available at compile-time... and what's more, it's got to be included verbatim, meaning the entire class definition has to be stuffed into a .h file (or included .c file, shudder). That's a pretty big problem, in my mind, because it detracts from information hiding and modularity.


--
Go be impersonally used as an organic semen collector!  (porkchop_d_clown)
templates and operator overloading (5.00 / 3) (#184)
by ZorbaTHut on Tue May 27, 2003 at 07:46:16 PM EST

for instance, if the SML/NJ compiler sees the function definition

fun f(a,b) = a + 2*b

it is smart enough to realize that a and b must be numbers and the result type must also be a number

Which would be rather nice if a and b were, necessarily, numbers, but it's worth pointing out that with C++, no such restrictions exist.

C++ also supports operator overloading - put simply, I can write my own custom classes that support basic arithmetic completely transparently. "a + 2 * b" could be integers or floats, or it could be Bob's Custom Big Integer Class, or it could be an arbitrary-precision floating-point class. Or, for that matter, it could even be a calculation involving vectors or matrices.

The assumption "oh, it involves math, it must be numbers" is one that C++ just plain can't make, because it's not true - and yes, this is a feature that I've used and abused beyond all reason, so I consider it a Very Good Thing (tm).

As for the error reporting, I'll admit it's been a weak point for quite a while - however, compilers are getting much better about this now, and the compiler I use at work gives much more useful error messages . . .

poly.cpp(642): error: expression must have class t ype
        in.test();
        ^
          detected during :
            instanti ation of "void interiorfunk(Data) [with Data=int]"  ;at line 646
            instanti ation of "void funk(Data) [with Data=int]"

This is a template function "funk", which calls another template function "interiorfunk", which tries to call the member function "test" on the data it's been given. I've passed funk an int (which obviously doesn't have a member function named "test") and it's giving me a somewhat useful error message. I say "somewhat" because it's still not telling me where the line that started the whole chain was - it knows that funk is calling interiorfunk at line 646, but it doesn't know where the first call of funk is (it's line 651, if you're curious).

So it's not perfect - but it is getting much better.

(Incidentally, the compiler is the Intel compiler - version 7.0)

The real problems with C++ templates (4.00 / 2) (#198)
by vladpetric on Tue May 27, 2003 at 10:11:37 PM EST

1. Time of compilation.

If your template resides in a header file, everything's fine - the compiler can do "text replacement" and generate fairly efficient code. This is not the case if you want to compile incrementally (it's so much tougher to do optimizations at link time)

2. Too much syntax.

Templates should have been limited to types, and only to classes.

Recursive templates, like the one described in an article are obfuscation to the highest degree. I don't see why you can't just use the preprocessor.

Function templates are nice for stuff like min(a, b), but if you combine function templates with class templates stuff can become really messy.

I've seen, in a very respectable book, a class template which had a template-ized constructor (different template than the main one). It achieved a cool thing, but the price it payed in readability/maintainability suffered a lot.

In that respect, the new Java coming from Sun does much better - you can only do so much (you can't shoot yourself in the foot)

3. C++ references.

C++ references are actually what I consider the worst features of the language. They're dumb (they can't be NULL, they can't be set more than once) - so you can't for instance do higher level structures with them, like AVL/red-black trees and furthermore they break the nice invariant of C - pointers are passed as pointers, everything else is copied (In other words, when you program
with references, you've gotta be very careful or you risk having different argument passing semantics, or even worse, reference to some dead stack).

Most template libraries (including STL) work with references, instead of pointers. What behaviour should a programmer expect from the following class:

set<myclass>

In what way should this be different from:

set<myclass&> or set<myclass*>?

What if a method of set returns a reference ?

(the answers to these questions are pretty clear for the stl set, the obfuscation problem still remains, though)

4. Developers

Templates and operator overloading are the 2 most abused features of the C++, because of their coolness factor (makes people feel "in-control" ...). Abusing OO-features usually makes an OO program much worse in terms of maintainability than a  non-OO one. It's one of the reasons OO projects fail.

Some of the #4 people would argue that all these points are bullshit, and if you're careful enough, you can avoid them. I'd remind them that coding is only 35% of the development cycle - the rest is debugging and maintaining.

Conclusion: C++ - the most obfuscated real language ever (only brainfuck does worse, but it's a synthetic one).

wrt to doing calculations at compile-time... (4.50 / 2) (#222)
by goonie on Wed May 28, 2003 at 01:44:16 AM EST

Isn't there a technique called "partial evaluation" that allows a compiler to identify bits of code that can be evaluated at compile time, evaluate them, and then insert the results into the generated code *without* the need to perform all this template/macro wackiness?

problem (3.25 / 4) (#225)
by Cruel Elevator on Wed May 28, 2003 at 02:55:26 AM EST

with one of your example. The correct example is given below:

int myCleverFunction() {
return 42;
}

It's 42, not 4, OK?

Sincerely,

Cruel Elevator.

My views (5.00 / 2) (#228)
by statusbar on Wed May 28, 2003 at 03:43:27 AM EST

Very good article, however it does not go far enough.  

I first started with c++ by porting gnu g++ v1.35 to my 4 meg Atari Mega ST with a 40 meg hd back in 1990.  My god what a mess, took 24 hours to build too!  I've always been a c++ nut, excited about every new feature and syntax that was added.

Until now.

A further problem is how the c++ standard has evolved.  One can argue that it is a better, more complete language now than it was in 1990.  But programming c++ was always like trying to hit a moving target.

A common misconception:  "This code is object oriented and therefore reusable!"

The reality:  "This code is organized for how c++ circa 1990 works.  It needs to be refactored again, otherwise it will not work with our new libraries - it isn't const correct or exception safe at all and our containers are now STL."

The complexity of c++ makes it even tougher for compilers to be correct.  I am amazed at how complete some of the compilers are now. But almost all of them are still incomplete.

Portability is a big problem.  I understand the need for ANSI/ISO to pass on specifying anything about multithreading behavior, but in reality many programs (right or wrong) need threads.  Some versions of STL were not thread safe, in obscure ways. Some versions of g++ did not have thread safe exceptions.  These are really tricky problems to solve when your code is correct but the compiler or library is the one spitting out thread-unsafe code.

My personal objective now is to use c and c++ to implement an embedded scripting language parser, like www.lua.org or scheme, or maybe even python.  Write the heavy duty algorithms in c++ and make them callable by the scripting language. Then write all the procedural code in the scripting language. Embedded all in one executable.  Anyone who thinks this would be too slow should analyze the design of Quake3 for a bit.

But what do I know, I'm still using C++ for embedded web-apps as well as for firmware on TI's 6701 DSP's.

--jeff++

Not only the templates are ugly (4.00 / 2) (#231)
by jope on Wed May 28, 2003 at 04:06:12 AM EST

... it is the whole language that is a terrible ugly hack and anachronism. The only excuse for using it is it's somewhat-backwardcompatibility to C, which is even more ugly and anachronistic, but - alas - *very* widespread. If you want beauty of concept take one of the many modern languages, some of them even useful for real developing, like Ocaml (www.ocaml.org). A whole universe of easyness and things you can do like you would expect will open up :)

Functors != Interfaces (none / 0) (#236)
by det on Wed May 28, 2003 at 07:47:55 AM EST

Functors and interfaces do different things. For example, pretend you wanted to write a 3d engine that had a list of objects that knew how to render themselves. With C++ you could have an abstract class called Renderable with a single pure virtual function named render and just have a STL list paramaterized by Renderable and any class that was derived from Renderable could be inserted. Similarily in haskell you could have a a type class named Renderable and have a List paramaterized by that. With ML however it doesnt seem possible. You could have a signature called RENDERABLE and a functor List(T: RENDERABLE) but then you have to initiate that to a paticular type like say:
structure L = List(struct
type t = sphere
val renderT = renderSphere
end);
Then you can only call L.render which is specific to sphere. You can never say RENDERABLE.render(foo). I personally think ocaml would be the coolest language in the world if it allowed some way to do interfaces while ditching it's crumby class system. Please correct me if there is a way to do this with functors!

Whack template (3.33 / 3) (#246)
by darthaya on Wed May 28, 2003 at 11:41:11 AM EST

And you lose STL, the best part about C++.

Then you go back to the ugly C, where everyone does his/her own implementation of string/vector/list/etc., what a waste of brain cells.


Other way to solve it, in an OO language: Eiffel (5.00 / 5) (#250)
by trixx on Wed May 28, 2003 at 12:09:59 PM EST

The article mentions ML and Lisp variants as languages with proper solutions of the problem that templates try to solve. One could argue that it's comparing apples and oranges, so I would like to extend the article with a comparison to a language that tries to be in a similar niche to C++ (compiled OO-imperative language): Eiffel

Don't get me wrong, I'm not against the functional paradigm (I've coded some ML/Lisp and a lot of Haskell), but I believe they are useful just for a small problem set, while I see OO more general-purpose.

Nobody in their right mind will think "Well, do I use C++ or Lisp for this project?", because the problem will probably dictate clearly one of the two. On the other hand, I wanted to show you about Eiffel, a language that you could say "Do I use C++ or Eiffel for this project?", in most cases that C++ is used.

Eiffel is not much newer than C++ (first language revision dates from 1986), but gets a lot of things done right wrt C++. It is a pure OO compilable language, and compilers generate code with similar efficiency to C++ (with method dispatch usually faster). It's a simple language, with a set of small powerful features instead of the C++ philosophy of "one feature for everybody"

One of the features is type genericity, and it's used to solve most of the same problems that templates try to solve. If you don't use it, you get things like class INTEGER_LIST_NODE
-- This class is not complete, only for illustrative purposes
creation
make
feature
make (i: INTEGER; n: INTEGER_LIST_NODE) is
do
item := i
next := n
end
feature
item: INTEGER
next: INTEGER_LIST_NODE
end

The above is what you would do in C... A list node of a linked list with a fixed type. e know the problems of that, so let's use inheritance to make something like thee Java solution class UNTYPED_LIST_NODE
creation
make
feature
make (i: ANY; n: UNTYPED_LIST_NODE) is
do
item := i
next := n
end
feature
item: ANY
next: UNTYPED_LIST_NODE
end

Note than Eiffel's ANY is like Java's Object, the parent class of everything.

This is a little better in the sense that I don't have to rewrite code for each type. Since anything (even INTEGER) inherits from ANY, i could code: new_list,other_list: UNTYPED_LIST_NODE
....
create new_list.make (42, other_list)
create new_list.make ("Hello world", new_list)
create new_list.make (new_list, new_list)

Here we can see that UNTYPED_LIST_NODE is... well, untyped. What if we want lists of an specific type? We can't. The example above connects node with an INTEGER, a STRING, and an UNTYPED_NODE_LIST. That's the Java solution. Besides, when you want to take something out of a list, you have to cast it into an useful types (because there's not much you can do with an Object/ANY). You could do that... there's a form of cast in Eiffel (not exactly a cast, and always safe), but there are better ways to do things. Actually, Eiffel casts are rarely used (usually when interacting with the outside nontyped world, like files), and I've used them less than once per programming project.

So, what's the right solution? we can use the genericity mechanism. At first sight you can find it similar to templates, but let's compare a little. First, the generic class example: class LIST_NODE [T]
creation
make
feature
make (i: T; n: LIST_NODE [T]) is
do
item := i
next := n
end
feature
item: T
next: LIST_NODE [T]
end

I could now declare things like LIST_NODE[ANY] (equivalent to UNTYPED_LIST_NODE), LIST_NODE[INTEGER] (equivalent to INTEGER_LIST_NODE), or LIST_NODE[any type you have in Eiffel].

This looks very similar to a template with parameter T, with expansion of T with its actual generic parameter. But there are differences:

  • Type checking: The 'generic class' (That is what things like LIST_NODE are called) are processed once, and safely type checked independently of its instantiations
  • No code is replicated. The code is generated once, usually. There are exceptions when you use expanded types (expanded types are types that are usually used by copy instead of by reference, like INTEGER or BOOLEAN); in that case different code is generated to make the internal representation of LIST_NODE objects more tight (so, internally, LIST_NODE is usually a pair of pointers, but if you use LIST_NODE[INTEGER], you get a pair with an int and a pointer).
  • Generic classes is a method only for classes. Eiffel has no equivalent to template functions. Generic parameters can only be types. That is more limited, but helps to have good type checking, and I have never needed more.

OK. Now i know that if I have a l: LIST[INTEGER], l.item is INTEGER (the compiler knows that statically, so i could write l.item+l.next.item w/o worring about casts (like C++, unlike Java). What if i need special properties of the generic parameter (the [T]) inside the class? for example class VECTOR2 [T]
-- Addable vector with 2 T components
-- Does NOT work
creation
make
feature
make (xx, yy: T) is
do
x := xx
y := yy
end
feature
x,y: T
feature
infix "+" (other: VECTOR2[T]) is
do
create Result.make (x+other.x, y+other.y)
end
end

Note that we can define operators as methods, and are dinamically dispatched like any normal method (all Eiffel methods are 'virtual'); there is no static overloading in Eiffel (the thing known in C++ as overloading is static overloading).

The above class VECTOR2 won't compile? what's wrong with it? The compiler says: ****** Error: Current type is ANY. There is no feature infix "+"
in class ANY.
Line 16 column 45 in VECTOR2 (./vector2.e) :
create Result.make (x+other.x, y+other.y)
^

Note that the message is quite clear... given that we know nothing about 'T', the static type checker assumes it is an ANY, and ANY doesn't have an infix '+' operator. so what we wanted to do actually was: class VECTOR2 [T -> NUMERIC]
-- Addable vector with 2 components
creation
make
feature
make (xx, yy:T) is
do
x := xx
y := yy
end
feature
x,y: T
feature
infix "+" (other: VECTOR2[T]): VECTOR2[T] is
do
create Result.make (x+other.x, y+other.y)
end
end

Note that above we said T->NUMERIC instead of just T. With that, we mean that we cannot use any T, but only those that inherit NUMERIC (NUMERIC is a standard abstract class describing ring-like structures: operators '+' and '*', features 'zero' and 'one' that are neutral elements, and things like that), so we now can declare VECTOR2[INTEGER] and VECTOR2[REAL]. Note that we can not declare VECTOR2[STRING], even when STRING has a + operator (for concatenation), because STRING is not numeric. So type checking is not based in the names of the operators but in its semantics instead (a NUMERIC + is not the same that a STRING +) C++ cannot make that difference.

We could also improve the previous class like this: class VECTOR2 [T -> NUMERIC]
-- Addable vector with 2 components
inherit
NUMERIC
creation
make
feature
make (xx, yy:T) is
do
x := xx
y := yy
end
feature
x,y: T
feature
infix "+" (other: VECTOR2[T]): VECTOR2[T] is
do
create Result.make (x+other.x, y+other.y)
end
-- Other operators, zero, one defined here ...
end

and then we can now even do a: VECTOR2[VECTOR2[MATRIX[INTEGER]]]

(assuming that MATRIX is a class inheriting NUMERIC).

Note that we achieved the same results of ML or LISP, in an OO language that could be a replacement for C++ in real-world situations. Learning Eiffel is not very difficult if you already know C++ or Java (It's a simpler language, so you have only to get a couple of powerful concepts).

Also note that all this can be achieved without type inference. Type inference is a nice feature of several functional languages, but it's not needed to get generic types, as I've shown above.

Well, it is long but I hope to show that there's a lot more Object-Orientation than what you see in C++, and that it can be done much more elegantly.

My semi-famous C++ rant (3.25 / 4) (#255)
by Eric Green on Wed May 28, 2003 at 01:58:50 PM EST

I posted this to rec.arts.sf.written some time in the past when we were discussing what computer languages would look like in the future, and it has circulated around the world in various quotes files ever since:

"C++ is an atrocity, the bletcherous scab of the computing world, responsible for more buffer overflows, more security breaches, more blue screens of death, more mysterious failures than any other computer language in the history of the planet Earth. It is pathetic, pitiful, a bag of disparate bolts on the side of "C", a fancy preprocessor that attempts to make "C" look like an object-oriented language and ends up merely being pathetic. If there was any mercy in this world, we would all have adopted Objective "C" as our standard object-oriented "C" follow-on and left C++ to the garbage bin of history where it belongs. Instead, we have a language more bloated than PL/1 or Ada, whose runtime library has all the coherency of a madman cutting pieces of books out and pasting them together into the documentation for the inconsistent drivel that comprises the standard C++ library, we have binding and linkage conventions that are utterly ridiculous in a supposedly "object-oriented" language, and otherwise a pathetic, ridiculous, drooling moronic abortion of computer science that should have been given a decent burial long ago (and would have been, if Microsoft had not mysteriously decided to standardize upon C++ to write their operating systems).

As for what languages are better than C++, gosh, what languages are NOT better than C++? Basically, any language whose basic design eliminates the possibility of memory leaks, whose semantics are simple enough for mere mortals to not have to peruse the 12,000 pages of Stroustrup to understand, that has a coherent and consistent and well-documented runtime library and a well-thought-out syntax, that has "real" objects instead of a wrapper around "C" structs, that does not allow buffer overflows to crash or, worse, subvert your program. What language is that? Oh, pretty much anything, actually, other than C++. Python, Ruby, Java, Objective CAML (which, BTW, has a compiler that actually generates faster code than many "C" compilers!), and many, many other languages that actually have a design that makes sense, which nobody has accused C++ of doing. C++ is a kludge, a hack, a bag on the side of "C", and always will be, and nothing we say or do will ever make that different."

-Eric Lee Green (eric@badtux.org) in rec.arts.sf.written
--
You are feeling sleepy... you are feeling verrry sleepy...

I wish Ocaml had C++-style templates (none / 0) (#266)
by cwitty on Wed May 28, 2003 at 05:36:31 PM EST

I hope my comment subject is sufficiently provocative. :-)

Often, when I'm programming in C++, I wish that it had ML-style generic polymorphism or Lisp-style macros.  Sometimes, when I'm programming in Ocaml, I wish it had C++-style templates.

For data structures, I agree that ML-style polymorphism is usually a better choice; with a couple of caveats.  

First, it's easier to write code which is portably efficient with C++ than with ML.  A ML implementation of a vector type (based on generic polymorphism) will be very inefficient when applied to characters, in some implementations of ML: it will use at least 4 times as much memory as it "should", and maybe more.  Some ML compilers may automatically generate a specialized implementation, which does not have this overhead (I'm not sure here; do real ML compilers do this specialization?); but many won't.  The C++ template version doesn't have this problem.

Second, the issue of interfaces is not as one-sided against C++ as you imply.  The exact problem you describe (where a List type has an output routine, but calling the output routine only works if the base type also has an output routine) can also be seen as a feature.  It means that you can provide an output routine as an optional feature of your container class, but still use the class with objects that can't be printed.  This is a useful, powerful feature of C++ that I don't see how to easily emulate with ML (the approaches I can think of lose modularity or run-time efficiency).  (I would prefer a way of having this feature that still let you completely typecheck the library code.  Maybe something like ML functors with optional arguments.)

I'm not sure what "This system [ML functors] gives you more abstraction power than C++ templates ..." means.  It seems to me that any ML functor can be translated (almost automatically) into a C++ template.

As far as comparing C++ templates to Lisp/Scheme macros, I would say that they're not really comparable; there are many things which you can do with one but not the other.  The main thing you can do with templates that you can't do with any other macro-style system I know of is compile-time dispatch based on the compile-time types of the chunks of program being manipulated.  Lisp can't really do this, since Lisp doesn't really have a compile-time type system.  (Maybe you could add a type inference system into your macro expansion, but the result would be clumsy and fragile.)  It's difficult in ML (or any other language that uses type inference) as well, since you would have to mix macro expansion and type inference somehow.

Here's a template example:

  template<typename T>
  void print_elements(T first, T last) {
    while (first != last) {
      cout << *first << ' ';
      first++;
    }
  }

This will print the elements of a C++ array, a STL vector, a STL linked list, or any of a wide variety of other containers, and it will do so whether the elements are integers, floating-point numbers, or some user-defined type.  And the container element access will not involve run-time virtual function calls or type dispatch.  I don't know of any other language where such code could be written so concisely and be so efficient (which is not to say that such languages don't exist).

In conclusion: I like OCaml much better than C++.  Basically, the only C++ feature I miss when writing OCaml code is templates.  Most uses of templates could be replaced by ML generic polymorphism or ML functors, but the result would likely be significantly less efficient with many (perhaps all) ML compilers, and would in some cases be far more verbose.  I would love to have a language in the ML family that supported C++-style templates (but this would be difficult, since the more interesting features of templates are difficult to combine with type inference).

Two words: Dynamic Typing (3.33 / 3) (#277)
by PolyEsther on Thu May 29, 2003 at 04:35:42 AM EST

It seems to me that dynamic typing is a much nicer alternative to C++'s templates.  

It would have been nice if Objective C had won out as the popular Object Oriented flavor of C.  C++ is a monstrosity and templates only add to the complexity of the beast (and at best, they are only a poor way of emulating the power of dynamic typing).

Dynamically typed languages like ObjC, Ruby and SmallTalk allow for much greater flexibility in program design.  In those languages, if an object doesn't respond to a particular message then it's not the correct type - type is determined by what messages an object will respond to.  But even then, all is not lost: by defining a 'method_missing' method in your class (In Ruby, but both SmallTalk and ObjC have similar facilities) you can define what happens when your object receives a  previously unknown message.  I've heard it put this way: When you walk down the street and someone says 'Blarg?' to you, do you don't just drop dead in your tracks because you don't know the meaning of 'Blarg?' - that's what happens in C++, the program drops dead.

In dynamically typed languages, you can  define several classes which have the same interface (respond to the same messages, or a subset of messages) and then use them in, for example, a collection, iterate through the collection sending the same message(s) to each object in the collection and not worry that each of those objects should be somehow related in a class hierarchy.  It's a very freeing concept, though perhaps a little scary at first if you're coming from a statically typed mindset, but once you get used to it you hate it when you have to go back to the confines of C++.

boost (4.50 / 2) (#279)
by mitch0 on Thu May 29, 2003 at 08:08:49 AM EST

I read most of the comments, yet I didn't see any reference to the boost library.

It's a wonderful library with lots of neat things done with templates. Many of the complaints mentioned in the comments are addressed.

I'll just mention the Concept Check Library for those who complain about unintelligible error messages.

I still think that C++ is one of the most powerful languages nowadays. Sure, you have to watch out, but at least you CAN do almost anything.

anyway, check out the boost site.
cheers, mitch

FORTH.... (5.00 / 1) (#282)
by Alhazred on Thu May 29, 2003 at 02:19:25 PM EST

I'm simply amazed that out of all the 1000's of comments here nobody mentioned anything about FORTH...

The Forth "Outer Interpreter" is a beautiful example of accomplishing the same goals by turning the system inside-out.

For those who don't know anything about FORTH, it is  a hybrid system. An "Outer Interpreter" parses an input stream (usually be default a console in the old days) and executes each token (called a word in FORTH parlance, but equivalent to a function) it finds.

This interpreter has a 2nd state, compile mode, in which it generates (virtual) machine code. A programmer can thus initiate compilation simply by  changing this flag. Normally this is accomplished by the word ":" (colon) which adds a new header to the dictionary (linked list of available words) and sets compile mode on, parsing the next token from the input stream to use as the name of this newly defined word. Thus:

: MYWORD + . ;

defines a new word (function) called MYWORD which calls the words '+' (plus), and '.' (dot) in sequence. The last word ';' (semi-colon) is a 'compiling' word. It is marked in the dictionary as such and thus the FORTH compiler simply executes it (just as it would in interpreter mode). Semi-colon flips the state back to interpreting, and may do some clean up as well in some FORTH implementations.

The beauty of the whole system is that awkward constructs like templates or LISP style macros are totally unneeded. One can simply dodge back and forth between compiling and interpreting as required.

For instance assuming we had a word factorial we could put 3! as a constant into another function quite easily.

: MYWORD
   [INTERPRET] 3 factorial LITERAL [COMPILE]
   dosomethingelse
;

Notice how brutally simple FORTH is. This is the strength of simplicity. You just plain tell the compiler/interpreter what to do next.

With these simple tools it is quite easy (almost trivial) to create an object-oriented FORTH or almost any other kind of extension. As with most LISPs FORTH is almost always written in itself except for a small subset of core functions. An entire working FORTH compiler and language can be implemented in less than 10000 lines of code, including the core!
That is not dead which may eternal lie And with strange aeons death itself may die.

Missed a key feature of templates. (none / 0) (#300)
by metalotus on Sun Jun 01, 2003 at 04:10:33 PM EST

The author mentioned generic and reusable components, and then discussed the various ways programming languages support type features. C++ templates create resuable binary code. For example, STL binary components are significantly different then modules in the Java library. A discussion about language support for type features does not capture the idea of reusable binary components. I think binary components are are most significant feature of STL, and this was not understood in the above article.

Generative Programming and C++ template LISP (none / 0) (#320)
by Leimy2k on Tue Jun 10, 2003 at 04:13:29 PM EST

The authors of the book Generative Programming had an implementation of Lisp using the recursive properties of C++ templates. I think such a meta-configuration-language is an intersting way to use templates.

Also doesn't Java 1.5 have some templatey thing coming out soon. The syntax looks awfully familiar.

At Los Alamos National Labs there is a template meta library called PETE [Portable Expression Template Engine] which is pretty powerful stuff. It can help optimize the heck out of your template code by eliminating unnecessary copies etc etc.

C++ often gets a bad review to the point where it almost seems very trendy to dislike it for very strange reasons. I have heard "I don't like using the bit shift operator for cout/cerr" but these are the same people who like the [] methods of Objective-C [which to me is an alien syntax that takes getting used to]. I suppose if they said operator overloading in general was a bad and unnecessary thing I could agree with them more since its nothing but syntactical sugar anyway... and maybe that sugar does indeed add more complexity than is necessary to the language to get work done. I mean... just compare the size of Bjarne's book to K&R's. There is definitely more to understand and more to get wrong about C++. It just takes a different kind of person to like that sort of thing I suppose.



Templates? Yes. Metaprogramming? Ick! (none / 0) (#322)
by ksandstr on Sat Jun 14, 2003 at 10:57:51 AM EST

The following is just my opinion, of course.  Couldn't be any other way.

Back in the day, I though that C++ templates were a pretty good thing.  I mean, type-agnostic collection/mapping/whathaveyou classes written in such a way that the compiler can inline most of the iteration primitives that you're going to use, type-safely no less? Gimme!

But these days, you have to wonder if the whole C++ template metaprogramming thing isn't just some elaborate, bizarre form of mental masturbation. I mean, sure, it's pretty cool how you can instruct the compiler (although in a syntactically clunky way) to generate this many iterations into this function, and that when constructing some classes that have a particularly high performance requirement, you can parameterize things like array sizes, invariants and so on. But run-of-the-mill unrolling? The Boost lambda module? "Traits" classes? Maybe it's just my lack of understanding (which, honestly, I'm not planning to improve at any specific time in the future) but shouldn't these things be provided by a sufficiently smart combination of language and compiler that would among other things treat any function, class or other language construct as generic and instantiate as desired?


Fin.

What's wrong with C++ templates? | 324 comments (253 topical, 71 editorial, 0 hidden)
Display: Sort:

kuro5hin.org

[XML]
All trademarks and copyrights on this page are owned by their respective companies. The Rest © 2000 - Present Kuro5hin.org Inc.
See our legalese page for copyright policies. Please also read our Privacy Policy.
Kuro5hin.org is powered by Free Software, including Apache, Perl, and Linux, The Scoop Engine that runs this site is freely available, under the terms of the GPL.
Need some help? Email help@kuro5hin.org.
My heart's the long stairs.

Powered by Scoop create account | help/FAQ | mission | links | search | IRC | YOU choose the stories!