Whereas dynamic_cast tests the downcasts and returns the null pointer or throws an exception on failure, static_cast just performs the necessary pointer arithmetic and leaves it up to th
Trang 1void failure_is_error(base1* p) {
try {
some_other_class& soc=dynamic_cast<some_other_class&>(*p);
// Use soc
}
catch(std::bad_cast& e) {
std::cout << e.what() << '\n';
}
}
void failure_is_ok(base1* p) {
if (some_other_class* psoc=
dynamic_cast<some_other_class*>(p)) {
// Use psoc
}
}
In this example, the pointer p is dereferenced[5] and the target type of the
conversion is a reference to some_other_class This invokes the throwing version of dynamic_cast The second part of the example uses the
non-throwing version by converting to a pointer type Whether you see this as a clear and concise statement of the code's intent depends upon your experience Veteran C++ programmers will understand the last example perfectly well Will all of those reading the code be sufficiently familiar with the workings of dynamic_cast, or
is it possible that they'll be unaware of the fact that it works differently depending
on whether the type being converted is a pointer or reference? Will you or a
maintenance programmer always remember to test for the null pointer? Will a maintenance programmer realize that dereferencing the pointer is necessary to get the exception if the conversion fails? Do you really want to write the same logic every time you need this behavior? Sorry for this rhetoricits intent is to make it painfully obvious that polymorphic_cast makes a stronger, clearer statement than dynamic_cast when a conversion failure should result in an exception It either succeeds, producing a valid pointer, or it fails, throwing an exception
Simple rules are easier to remember
[5]
If the pointer p is null, the example results in undefined behavior because it will dereference a null pointer
We haven't looked at how you can overload polymorphic_cast to account for unusual conversion needs, but it should be noted that it's possible When would you want to change the default behavior of a polymorphic cast? One example is for
Trang 2handle/body-classes, where the rules for downcasting may be different from the default, or should be disallowed altogether
Summary
It is imperative to remember that others need to maintain the code we write That means that we have to make sure that the code and its intent are clear and
understandable In part, this can be accomplished by annotating the code, but it's much easier for everyone if the code is self-explanatory polymorphic_cast documents the intent of code more clearly than dynamic_cast when an
exception is expected for failed (pointer) conversions, and it makes for shorter code If a failed conversion isn't considered an error, dynamic_cast should be used instead, which makes use of dynamic_cast clearer, too Using
dynamic_cast as the only means of expressing these different purposes is error prone and less clear The difference between the throwing and non-throwing
version is too subtle for many programmers
When to use polymorphic_cast and dynamic_cast:
When a polymorphic cast failure is expected, use dynamic_cast<T*> It makes clear that the failure is not an error
When a polymorphic cast must succeed in order for the logic to be correct, use polymorphic_cast<T*> It makes clear that a conversion failure is
an error
When performing polymorphic casts to reference types, use
dynamic_cast
polymorphic_downcast
Header: "boost/cast.hpp"
Sometimes dynamic_cast is considered too inefficient (measured, I'm sure!) There is runtime overhead for performing dynamic_casts To avoid that
overhead, it is tempting to use static_cast, which doesn't have such
performance implications static_cast for downcasts can be dangerous and
Trang 3cause errors, but it is faster than dynamic_cast If the extra speed is required,
we must make sure that the downcasts are safe Whereas dynamic_cast tests the downcasts and returns the null pointer or throws an exception on failure,
static_cast just performs the necessary pointer arithmetic and leaves it up to the programmer to make sure that the conversion is valid To be sure that
static_cast is safe for downcasting, you must make sure to test every
conversion that will be performed polymorphic_downcast tests the cast with dynamic_cast, but only in debug builds; it then uses static_cast to
perform the conversion In release mode, only the static_cast is performed The nature of the cast implies that you know it can't possibly fail, so there is no error handling, and no exception is ever thrown So what happens if a
polymorphic_downcast fails in a non-debug build? Undefined behavior Your computer may melt The Earth may stop spinning You may float above the clouds The only thing you can safely assume is that bad things will happen to your program If a polymorphic_downcast fails in a debug build, it asserts on the null pointer result of dynamic_cast
Before considering how to speed up a program by exchanging
polymorphic_downcast for dynamic_cast, review the design
Optimizations on casts are likely indicators of a design problem If the downcasts are indeed needed and proven to be performance bottlenecks,
polymorphic_downcast is what you need You can only find erroneous casts
in testing, not production (release builds), and if you've ever had to listen to a
screaming customer on the other end of the phone, you know that catching errors
in testing is rather important and makes life a lot easier Even more likely is that you've been the customer from time to time, and know firsthand how annoying it is
to find and report someone else's problems So, use polymorphic_downcast
if needed, but tread carefully
Usage
polymorphic_downcast is used in situations where you'd normally use
dynamic_cast but don't because you're sure which conversions will take place, that they will all succeed, and that you need the improved performance it brings Nota bene: Be sure to test all possible combinations of types and casts using
polymorphic_downcast If that's not possible, do not use
polymorphic_downcast; use dynamic_cast instead When you decide to
go ahead and use polymorphic_downcast, include "boost/cast.hpp"
Trang 4#include <iostream>
#include "boost/cast.hpp"
struct base {
virtual ~base() {};
};
struct derived1 : public base {
void foo() {
std::cout << "derived1::foo()\n";
}
};
struct derived2 : public base {
void foo() {
std::cout << "derived2::foo()\n";
}
};
void older(base* p) {
// Logic that suggests that p points to derived1 omitted
derived1* pd=static_cast<derived1*>(p);
pd->foo(); // < What will happen here?
}
void newer(base* p) {
// Logic that suggests that p points to derived1 omitted
derived1* pd=boost::polymorphic_downcast<derived1*>(p);
// ^ The above cast will cause an assertion in debug builds
pd->foo();
}
int main() {
derived2* p=new derived2;
older(p); // < Undefined
newer(p); // < Well defined in debug build
}
The static_cast in the function older will succeed,[6] and as bad luck would have it, the existence of a member function foo lets the error (probably, but again,
no guarantees hold here) slip until someone with an error report in one hand and a debugger in the other starts looking into some strange behavior When the pointer
is downcast using static_cast to a derived1*, the compiler has no option but to trust the programmer that the conversion is valid However, the pointer passed to older is in fact pointing to an instance of derived2 Thus, the pointer
pd in older actually points to a completely different type, which means that
Trang 5anything can happen That's the risk one takes when using a static_cast to downcast The conversion will always "succeed" but the pointer may not be valid [6]
At least it will compile
In the call to function newer, the "better static_cast,"
polymorphic_downcast not only catches the error, it is also kind enough to pinpoint the location of the error by asserting Of course, that's true only for debug builds, where the cast is tested by a dynamic_cast Letting an invalid
conversion through to release will cause grief In other words, you get added safety for debug builds, but that doesn't necessarily mean that you've tried all possible conversions
Summary
Performing downcasts using static_cast is dangerous in many situations You should almost never do it, but if the need does arise, some additional safety can be bought by using polymorphic_downcast It adds tests in debug builds, which can help find conversion errors, but you must test all possible conversions to make its use safe
If you are downcasting and need the speed of static_cast in release builds, use polymorphic_downcast; at least you'll get assertions for errors during testing
If it's not possible to cover all possible casts in testing, do not use
polymorphic_downcast
Remember that this is an optimization, and you should only apply optimizations after profiling demonstrates the need for them
numeric_cast
Header: "boost/cast.hpp"
Conversions between integral types can often produce unexpected results For example, a long can typically hold a much greater range of values than a short,
Trang 6so what happens when assigning a long to a short and the long's value is outside of short's range? The answer is that the result is implementation defined (a nice term for "you can never know for sure") Signed to unsigned conversions between same size integers are fine, so long as the signed value is positive, but what happens if the signed value is negative? It turns into a large unsigned value, which is indeed a problem if that was not the intention numeric_cast helps ensure valid conversions by testing whether the range is preserved and by throwing
an exception if it isn't
7 The C++ Standard covers promotions and conversions for numeric types in
§4.5-4.9
Before we can fully appreciate numeric_cast, we must understand the rules that govern conversions and promotions of integral types The rules are many and sometimes subtlethey can trap even the experienced programmer Rather than stating all of the rules7 and then carry on, I'll give you examples of conversions that are subject to undefined or surprising behavior, and explain which rules the
conversions adhere to
When assigning to a variable from one of a different numeric type, a conversion occurs This is perfectly safe when the destination type can hold any value that the source can, but is unsafe otherwise For example, a char generally cannot hold the maximum value of an int, so when an assignment from int to char occurs, there is a good chance that the int value cannot be represented in the char
When the types differ in the range of values they can represent, we must make sure that the actual value to convert is in the valid range of the destination type
Otherwise, we enter the land of implementation-defined behavior; that's what
happens when a value outside of the range of possible values is assigned to a
numeric type.[8] Implementation-defined behavior means that the implementation is free to do whatever it wants to; different systems may well have totally different behavior numeric_cast can ensure that the conversions are valid and legal or they will not be allowed
[8]
Unsigned arithmetic notwithstanding; it is well defined for these cases
Usage
numeric_cast is a function template that looks like a C++ cast operator and is parameterized on both the destination and source types The source type can be implicitly deduced from the function argument To use numeric_cast, include
Trang 7the header "boost/cast.hpp" The following two conversions use
numeric_cast to safely convert an int to a char, and a double to a
float
char c=boost::numeric_cast<char>(12);
float f=boost::numeric_cast<float>(3.001);
One of the most common numeric conversion problems is assigning a value from a type with a wider range than the one being assigned to Let's see how numeric_cast
can help
Assignment from a Larger to a Smaller Type
When assigning a value from a larger type (for example, long) to a smaller type (for example, short), there is a chance that the value is too large or too small to
be represented in the destination type If this happens, the result is (yes, you've guessed it) implementation-defined We'll talk about the potential problems with unsigned types later; let's just start with the signed types There are four built-in signed integral types in C++:
signed char
short int (short)
long int (long)
There's not much one can say with absolute certainty about which type is larger[9] than others, but typically, the listing is in increasing size, with the exception that int and long often hold the same range of values They're all distinct types, though, even if they're the same size To see the sizes on your system, use either sizeof(T) or std::numeric_limits<T>::max() and
std::numeric_limits<T>::min()
[9]
Of course, the ranges of signed and unsigned types are different even if the types have the same size
When assigning one signed integral type to another, the C++ Standard says:
"If the destination type is signed, the value is unchanged if it can be represented in the destination type (and bitfield width); otherwise, the value is
Trang 8[10]
See §4.7.3 of the C++ Standard
The following piece of code gives an example of how these
implementation-defined values are often the result of seemingly innocent assignments, and finally how they are avoided with the help of numeric_cast
#include <iostream>
#include "boost/cast.hpp"
#include "boost/limits.hpp"
int main() {
std::cout << "larger_to_smaller example\n";
// Conversions without numeric_cast
long l=std::numeric_limits<short>::max();
short s=l;
std::cout << "s is: " << s << '\n';
s=++l;
std::cout << "s is: " << s << "\n\n";
// Conversions with numeric_cast
try {
l=std::numeric_limits<short>::max();
s=boost::numeric_cast<short>(l);
std::cout << "s is: " << s << '\n';
s=boost::numeric_cast<short>(++l);
std::cout << "s is: " << s << '\n';
}
catch(boost::bad_numeric_cast& e) {
std::cout << e.what() << '\n';
}
}
Utilizing std::numeric_limits, the long l is initialized to the maximum value that a short can possibly hold That value is assigned to the short s and printed After that, l is incremented by one, which means that it now holds a value that cannot be represented by a short; it is outside the range of values that a short can represent After assigning from the new value of l to s, s is printed again What's the value, you might ask? Well, because the assignment results in implementation-defined behavior, that depends upon the platform On my system, with my compiler, it turns out that the result is a large negative value, which
Trang 9implies that the value has been wrapped There's no telling[11] what it will be on your system without running the preceding code Next, the same operations are performed again, but this time using numeric_cast The first cast succeeds, because the value is within range The second, however, fails, and the result is that
an exception of type bad_numeric_cast is thrown The output of the program
is as follows
[11]
Although the behavior and value demonstrated here is very common on 32-bit platforms
larger_to_smaller example
s is: 32767
s is: -32768
s is: 32767
bad numeric cast: loss of range in numeric_cast
A benefit that might be even more important than dodging the implementation-defined value is that numeric_cast helps us avoid errors that are otherwise very hard to trap The strange value could be passed on to other parts of the
application, perhaps working in some cases, but almost certainly yielding the
wrong result Of course, this only happens for certain values, and if those values seldom occur, the error will be very hard to track down Such errors are insidious because they happen only for some values rather than all of the time
Loss of precision or range is not unusual, and if you aren't absolutely certain that a value too large or too small for the destination type will never be assigned,
numeric_cast is the tool for you You can even use numeric_cast when it's unnecessary; the maintenance programmer may not have the same insight as you
do Note that although we have covered only signed types here, the same principles apply to unsigned integral types, too
Special CaseUnsigned Integral Type As Destination
Unsigned integral types have a very interesting propertyany numeric value can be legally assigned to them! There is no notion of positive or negative overflow when
it comes to unsigned types They are reduced modulo the number that is one
greater than the largest value of the destination type Say what? An example in code might make it clearer
#include <iostream>
Trang 10#include "boost/limits.hpp"
int main() {
unsigned char c;
long l=std::numeric_limits<unsigned char>::max()+14;
c=l;
std::cout << "c is: " << (int)c << '\n';
long reduced=l%(std::numeric_limits<unsigned char>::max()+1);
std::cout << "reduced is: " << reduced << '\n';
}
The output of running the program follows:
c is: 13
reduced is: 13
The example assigns a value that is certainly greater than what an unsigned char can hold, and then that same value is calculated The workings of the
assignment is shown in this line:
long reduced=l%(std::numeric_limits<unsigned char>::max()+1);
This behavior is often referred to as value wrapping If you want to use this
property of unsigned integral types, there is no need to use numeric_cast in those situations Furthermore, numeric_cast won't accept it
numeric_cast's intent is to catch errors, and this is considered an error because
it is the result of a typical user misunderstanding If the destination type cannot represent the value that is being assigned, a bad_numeric_cast exception is thrown Just because unsigned integer arithmetic is well defined doesn't make the programmer's error less fatal.[12] For numeric_cast, the important aspect is to preserve the actual value
[12]
The point: If you really want value wrapping, don't use numeric_cast
Mixing Signed and Unsigned Integral Types
It's easy to have fun[13] when mixing signed and unsigned types, especially when performing arithmetic operations Plain assignments offer some clever pitfalls, too The most common problem is assigning a negative value to an unsigned type The result is almost certainly not what was intended Another issue is when assigning from an unsigned type to a signed type of the same size Somehow, it seems to be