Accustoming Yourself to C++

Accustoming Yourself to C++

Reference: Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs

Prefer consts, enums, and inlines to #defines

Prefer the compiler to the preprocessor because #define may be treated as if it is not part of the language per se.

1
2
3
4
5
6
#define TEMP 1.65 // It may never be seen by compilers -> may not in symbol table -> causes problem.
// So replace as below is better
const double Temp = 1.65;
// uppercase usually for macros
// above def seen by compilers and be in symbol tables
// const float may yield smaller code than #define

When replacing #define with constants be aware:

  1. Defining constant pointers: Important that the ptr to be constant

    • const char * const varName = “value”;
    • varName is a const ptr that points const char which is “value”
    • For string: const std::string varName(“value”);
  2. Class-specific constants: ensure only one copy of it -> static like following

    • Usually CPP requires definition except class-specific constants of integeral type (int, chars, bools…).
      1
      2
      3
      4
      5
      6
      7
      8
      class Temp{
      private:
      static const int var = value;
      // constant declaration + definition
      // It may be illegal for older compilers, as was illegal to provide initial value for static member. If that,
      static const int old;
      };
      const int Temp::old = 1; // def goes in impl. file
  3. If, somehow, statis must have value during compilation, use enum hack

    • Somehow behaves more #define than a const
    • It’s legal to take address of const, but enum is not.
    • Typically, not legal to take address of #define
    • So, if you don’t want user get a ptr or reference of your constants, use enum
    • Enum hack is a fundamental technique of template metaprogramming that you can know when you see it what is it for.
      1
      2
      3
      4
      class Temp{
      private:
      enum {VarName = 5}; // VarName a symbolic name for 5
      };
      Don’t use #define macro like MAX_IS(a,b) f((a…)), but use inline type template instead.
      1
      2
      3
      4
      5
      6
      7
      #define CALL_WITH_MAX ... 
      // Instead use
      template <typename T>
      inline void callWithMax(const T& a, const T& b){
      f(a > b ? a : b);
      }
      // Can have efficiency of macro (less overhead for calling a function) + predictable behaviour + type safety

Use const whenever possible

It allows you to specify a semantic constraint -> particular object not be modified

1
2
3
4
5
6
7
char greeting[] = "HI";
char *p = greeting; // non const ptr, non const data
const char *p = greeting; // non const ptr, const data
char * const p = greeting; // const ptr, non const data
const char * const p = greeting; // const ptr, data.
// Remember to read it from the right side.
// last one: p is a const ptr that points const char

Declaring an iterator const = declaring a const ptr
Check the following:

1
2
3
4
5
6
7
8
9
10
11
12
// itr = T * const
const std::vector<int>::iterator itr = vec.begin();
// Valid, and invalid
*itr = 10; ++itr;

// itr = const T*
std::vector<int>::const_iterator cItr = vec.begin();
*cIter = 10; // Invalid!
++cIter; // valid

// const iterator: a const iterator for a value
// const_iterator: an iterator for a const value
1
2
3
4
const Temp operator*(const Temp& lhs, const Temp& rhs);
// const here will return const Temp object which prevents the returned object to be modified
Temp operator*() const;
// const here will ensure no modification on this object as passed by const Temp& this implicitly. but can modify mutable member

To remember:

  • Declaring const -> helps compilers to detect usage errors.
  • Compilers enforce bitwise constness, but you should program using conceptual constness.
  • if const and non-const have same implementation, have non-cost and call const version.

Make sure to initalise objects before use

Very complicated to understand which one initialised or not -> so always initialise the objects. Dont get confused with assignment and initialisation.

1
2
3
4
5
6
7
8
Temp::Temp(a, b, c){
// all assignments
a_ = a;
b_ = b;
c_ = c;
}
Temp::Temp(a,b,c) : a_(a), b_(b) ...{} // initialisation
// Copy constructor is more efficient than default constructor + copy assignment operator call.

Order:

  • Base class is initialised before derived class.
  • Data member is initialised in the order of its declaration.

The order of initialisation of non-local static objects defined in different translation units.

  1. A static object:
    1. one that exists from the time it’s constructed until the end of the program (on heap).
      1. so stack and heap-based objects are thus excluded (as it can be deallocated before the end of the program)
      2. So include global objects, objects defined at namespace scope, objects declared static inside classes, and inside functions and objects declared static at file scope.
    2. Static objects inside functions: local static objects
    3. Others are: non-local static objects.
  2. A translation unit: source code giving rise to a single object file - a basic unit of cpp compilation (source file + files included as #include).

It becomes an issue when it involves at least two separately compiled source files: Each contains non-local static object with dependency because the relative order of initialization of non-local static objects defined in different translation units is undefined

Problem solved entirely by set both non-local static object to be local static objects -> singleton pattern.

  • CPP gurantee that local static objects are initialised when object’s definition is encountered when to call it.
  • By changing direct access to non-local static object to calls to functions (that returns local static..), problem gets solved.
1
2
3
4
5
6
7
8
9
// Example.
Temp& tmp(){
static Temp tp;
return tp; // return reference to this local static object
}
TempTwo::TempTwo(...){
std::size_t asdf = tmp().functionName();
// tmp() here is static object that is guaranteed to be initialised.
}

This reference-returning function is simple: define and initialise a local static object, and return.

  • It makes excellent candidates for inlining if called frequently.
  • It makes problematic in multithreaded systems; but it is just what non-const static object always problematic in any multithreaded systems.
    • To solve this, have to manually invoke the functions in single-thread to avoid initialisation-related race condition.

Summary

To avoid initialisation problems -> remember three:

  1. Manually initialise non-member objects of built-in types.
  2. Use member initialisation lists to initialise all parts of an object (In a constructor).
  3. Design to avoid that non-local static object defined in separate translation units problem.
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×