...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Front Page / Technical Details / Portability / Broken Integral Constant Expressions |
This is probably the most surprising of the portability issues we're going to discuss, not least because for many C++ programmers, their everyday experience seems to indicate no problems in this area whatsoever. After all, integer compile-time computations along the lines of:
enum flags { flag1 = (1 << 0) , flag2 = (1 << 1) , flag3 = (1 << 2) ... };
are very commonplace in C++, and there is hardly a compiler out there that cannot handle this correctly. While arithmetic by itself is indeed rarely problematic, when you are trying to mix it with templates on certain deficient compilers, all kinds of new issues arise. Fortunately, as with the rest of the portability issues we're discussing here, the problem fades into past as new compiler versions are released. The majority of most recent compilers of many vendors are already free from these issues.
The problem is in fact multi-faceted; there are a number of different subissues. Some are present in one set of compilers, some are in another, and it's not uncommon for a code that works for one compiler to break another one and vice-versa. If this sounds like a maintenance nightmare to you, it is! If you are interested in the specific list of issues, please refer to John Maddock's excellent "Coding Guidelines for Integral Constant Expressions" summary. For the purpose of our discission here, it is sufficient to say that if your code has to work on one of the compilers listed as problematic in this area, you can safely assume that if you decide to fight them on a case-by-case basis, chances are that you won't be able to maintain your sanity for very long.
On the positive side, when you have an issue with integral arithmetic, the diagnostics are almost always straightforward: usually the error message refers you to the exact place in the code that is causing problems, and the essence of issue is obvious from the error's text, or it becomes obvious once you've encountered the same error a couple of times. For instance, if we throw this well-formed fragment at MSVC 7.1 (otherwise an excellent compiler!)
void value(); // compares two Integral Constants template< typename N1, typename N2 > struct less : bool_< (N1::value < N2::value) > // line #8 { };
we get:
portability.cpp(8) : warning C4346: 'N2::value' : dependent name is not a type prefix with 'typename' to indicate a type portability.cpp(10) : see reference to class template instantiation 'less<N1,N2>' being compiled portability.cpp(8) : error C2143: syntax error : missing ',' before ')' portability.cpp(9) : error C2143: syntax error : missing ';' before '{' portability.cpp(10) : error C2143: syntax error : missing ';' before '}' portability.cpp(10) : fatal error C1004: unexpected end of file found
The errors themselves are far from being ideal, but at least we are pointed at the correct line and even the correct part of the line. The above basically reads as "something's wrong between the parentheses", and that plus the "syntax error" part is usually enough of the clue.
Despite the fact the problems are so numerous and multi-faceted and the workarounds are conflicting, the problems can be hidden reliably beneath a library abstraction layer. The underlaying idea is very simple: we can always wrap the constants in types and pass those around. Then all that is left is to implement algebraic metafunctions that operate on such wrappers, once, and we are home safe.
If this sounds familiar to you, probably it's because you have already took a look at the MPL and know that the approach we just described is in fact the standard way of doing arithmetic in the library. Although it's motivated by more general observations, this fact comes very handy for the library users that care about portability of their numerically-heavy metaprograms. The MPL primitives are already there, and more importantly, they already implement the necessary workarounds, so your numeric code just works. In fact, if you stay within the library's type-wrapper idioms, these particular problems never "leak" out of its abstraction layer.
On a final note, there is a price of avoiding built-in arithmetics altogether, namely decreased readability and, on some compilers, increased compile-time overhead. Still, in majority of cases, the benefits of type-based arithmetics overweight its small shortcomings.