Use of C++ Features
- Rule: Hunt Leaks
Use every possible means to release the resources you consume, especially memory. Valgrind can be a nice assistant to track memory leaks (see Valgrind, The Ultimate Memory Debugger). To demonstrate different memory management styles, you are invited to use different features in the course of your development: proper use of destructors for the AST, use of a factory for
symbol
,Temp
etc., use ofstd::unique_ptr
starting with theTranslate
module, and finally use of reference counting via smart pointers for the intermediate representation.- Rule: Hunt code duplication
Code duplication is your enemy: the code is less exercised (if there are two routines instead of one, then the code is run half of the time only), and whenever an update is required, you are likely to forget to update all the other places. Strive to prevent code duplication from sneaking into your code. Every C++ feature is good to prevent code duplication: inheritance, templates etc.
- Rule: Prefer
dynamic_cast
of references Of the following two snippets, the first is preferred:
const IntExp& ie = dynamic_cast<const IntExp&>(exp); int val = ie.value_get();
const IntExp* iep = dynamic_cast<const IntExp*>(&exp); assert(iep); int val = iep->value_get();
While upon type mismatch the second
abort
s, the first throws astd::bad_cast
: they are equally safe.- Rule: Use virtual methods, not type cases
Do not use type cases: if you want to dispatch by hand to different routines depending upon the actual class of objects, you probably have missed some use of virtual functions. For instance, instead of
bool compatible_with(const Type& lhs, const Type& rhs) { if (&lhs == &rhs) return true; if (dynamic_cast<Record*>(&lhs)) if (dynamic_cast<Nil*>(&rhs)) return true; if (dynamic_cast<Record*>(&rhs)) if (dynamic_cast<Nil*>(&lhs)) return true; return false; }
write
bool Record::compatible_with(const Type& rhs) { return &rhs == this || dynamic_cast<const Nil*>(&rhs); } bool Nil::compatible_with(const Type& rhs) { return dynamic_cast<const Record*>(&rhs); }
- Rule: Use
dynamic_cast
for type cases Did you read the previous item, “Use virtual methods, not type cases”? If not, do it now.
If you really need to write type dispatching, carefully chose between
typeid
anddynamic_cast
. In the case oftc
, where we sometimes need to down cast an object or to check its membership to a specific subclass, we don’t needtypeid
, so usedynamic_cast
only.- They address different needs:
dynamic_cast
for (sub-)membership,typeid
for exact typeThe semantics of testing a
dynamic_cast
vs. a comparison of atypeid
are not the same. For instance, think of a classA
with subclassB
with subclassC
; then compare the meaning of the following two snippets:// Is `a' containing an object of exactly the type B? bool test1 = typeid(a) == typeid(B); // Is `a' containing an object of type B, or a subclass of B? bool test2 = dynamic_cast<B*>(&a);
- Non polymorphic entities
typeid
works on hierarchies withoutvtable
, or even builtin types (int
etc.).dynamic_cast
requires a dynamic hierarchy. Beware oftypeid
on static hierarchies; for instance consider the following code, courtesy from Alexandre Duret-Lutz:#include <iostream> struct A { // virtual ~A() {}; }; struct B: A { }; int main() { A* a = new B; std::cout << typeid(*a).name() << std::endl; }
it will “answer” that the
typeid
of*a
isA
(!). Usingdynamic_cast
here will simply not compile [1].If you provideA
with a virtual function table (e.g., uncomment the destructor), then thetypeid
of*a
isB
.- Compromising the future for the sake of speed
Because the job performed by
dynamic_cast
is more complex, it is also significantly slower thattypeid
, but hey! better slow and safe than fast and furious.You might consider that today, a strict equality test of the object’s class is enough and faster, but can you guarantee there will never be new subclasses in the future? If there will be, code based
dynamic_cast
will probably behave as expected, while code basedtypeid
will probably not.
More material can be found the chapter 8 of see Thinking in C++ Volume 2: Run-time type identification.
- Rule: Use const references in arguments to save copies (EC22)
We use const references in arguments (and return value) where otherwise a passing by value would have been adequate, but expensive because of the copy. As a typical example, accessors ought to return members by const reference:
const Exp& OpExp::lhs_get() const { return lhs_; }
Small entities can be passed/returned by value.
- Rule: Use references for aliasing
When you need to have several names for a single entity (this is the definition of aliasing), use references to create aliases. Note that passing an argument to a function for side effects is a form of aliasing. For instance:
template <typename T> void swap(T& a, T& b) { T c = a; a = b; b = c; }
- Rule: Use pointers when passing an object together with its management
When an object is created, or when an object is given (i.e., when its owner leaves the management of the object’s memory to another entity), use pointers. This is consistent with C++:
new
creates an object, returns it together with the responsibility to calldelete
: it uses pointers. For instance, note the three pointers below, one for the return value, and two for the arguments:OpExp* opexp_builder(OpExp::Oper oper, Exp* lhs, Exp* rhs) { return new OpExp(oper, lhs, rhs); }
- Rule: Avoid static class members (EC47)
More generally, “Ensure that non-local static objects are initialized before they’re used”, as reads the title of EC47.
Non local static objects (such as
std::cout
etc.) are initialized by the C++ system even beforemain
is called. Unfortunately there is no guarantee on the order of their initialization, so if you happen to have a static object which initialization depends on that of another object, expect the worst. Fortunately this limitation is easy to circumvent: just use a simple Singleton implementation, that relies on a local static variable.This is covered extensively in EC47.
- Rule: Use
foo_get
, notget_foo
Accessors have standardized names:
foo_get
andfoo_set
.There is an alternative attractive standard, which we don’t follow:
class Class { public: int foo(); void foo(int foo); private: int foo_; }
or even
class Class { public: int foo(); Class& foo(int foo); // Return *this. private: int foo_; }
which enables idioms such as:
{ Class obj; obj.foo(12) .bar(34) .baz(56) .qux(78) .quux(90); }
- Rule: Use
dump
as a member function returning a stream You should always have a means to print a class instance, at least to ease debugging. Use the regular
operator<<
for standalone printing functions, butdump
as a member function. Use this kind of prototype:std::ostream& Tree::dump(std::ostream& ostr [, ...]) const
where the ellipsis denote optional additional arguments.
dump
returns the stream.
Footnotes