philosophy

  1. use STL
  2. express intent (const, parallel)
  3. statically safe: type of the data should be known to compiler (use {} to initialize)
  4. compile time check > run-time
  5. RAII : resource acquisition is initialization
  6. immutable data ( const )
  7. encapsulate constructs, better interface
  8. use more than one c++ compiler

Interfaces

avoid non-const global variable

for:

  1. testablity
  2. concurrency
  3. optimization

avoid Singleton. Use dependency injection

Dependency injection:

  • constructor
  • setter
  • (template param)
1
2
3
4
5
6
7
8
9
10
11
class Client{
...
private:
std::shared_ptr<Logger> logger; //base class
}

...
Client cl(std::make_shared<SimpleLogger>());//constructor
cl.log();
cl.setLogger(std::make_shared<TimeLogger>());//setter method
cl.log();

Good interfaces

  1. explicit
  2. precise\strongly typed
  3. low function params

could use Pimpl(pointer to implementation), if implementation is often changed. Avoid recompilation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Widget{
class impl; //declaration
std::unique_ptr<impl> pimpl;
public:
void draw();
...

}

class Widget::impl{
int n;
public:
void draw(const Widget& w){...};
}

void Widget::draw(){ pimpl->draw(*this)};

Function

Good name

verb+object / verb or too long

constexpr

evaluated at compile time.
inline
result -> read-only, thread safe
pure function

1
2
3
4
5
6
7
8
9
10
constexpr auto gcd(int a, int b){
...
}
int main(){
constexpr int i = gcd(11,121); ==> let compiler to calculate
//compare with
int a = 11;
int b = 121;
int j = gcd(a,b); ==> function call, more consume
}

use noexcept

if the function should not have exception. (if happens will terminate)

use pure function

same input -> same output
for :

  1. isolated test
  2. result cached
  3. reordered or executed on threads

parameter passing

I

1
2
3
void f1(const std::string& s)// use const &

void f2(int x) //use x

Forward

in factory function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename T, typename ... T1> //variadic templates
T create(T1&& ... t1){ //passing through
return T(std::forward<T1>(t1) ...);
}
struct Mytype{
Mytype(int, double, bool){}
};

int main{
int five = 5;
int myFive = create<int>(five);

int myFive2 = create<int>(5);

int myZero = create<int>();

Mytype t = create<Mytype>(myZero, 5.5, true);
}

More explanation:

  • compiler will deduce the T1 … typename by the arguments
    1
    create<Mytype>(myZero, 5.5, true);
    the create function will be deduced to:
    1
    Mytype create(int &&, double &&, bool &&){}
  • T1&& will perserve the lvalue or rvalue property when forwarding
  • use … before T1 to pack, after T1 to unpack
  • use std::forward to perfect forwarding a value. (copy when lvalue, move when rvalue);

IN and OUT (modify in function)

use non-const &

1
2
3
void addone(std::vector<int>& v){
v.push_back(6);
}

OUT

use struct or tuple
use structured binding to unbind (like python).

1
2
auto [iter, inserted] = mySet.insert(2011);
//insert will return a iterator to set and a success flag

Ownership semantic

  • func(value): func owns a copy of value
  • func(pointer* ) or func(ref& ): fumc borrows the ownership
  • func(std::unique_ptr): func becomes the only owner. (moved to function)
  • func(std::shared_ptr): func becomes a shared owner

the ownership should be documented => for destruction
the std::move of unique_ptr is for moving ownership

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
Myint myint{1998}; //prefer this type of init
Myint* ptr = &myint;
Myint& ref = myint; //these three share a same 'myint'
auto uniquePtr = make_unique<Myint>(2011);
auto sharedPtr = make_shareed<Myint>(2012);

funcCopy(myint);//copy one myint, and destruct the copy inside the func. //print"1998"
funcPtr(ptr); //reserve
funcRef(ref); //reserve

funcUniq(std::move(uniquePtr));//move the ownership, only one uniquePtr, not destructed
//after the funcUniq, the uniquePtr destructed. print"2011"

funcShare(sharedPtr); //reserve sharedCnt + 1
//after finish funcShare, sharedCnt -1

}//print"2012" (shared over)
//print"1998"

Return Value

  • Return a T* to indicate a position ( a tree node ), a linked list node …
  • chain operation, like << >> , return a T&, don’t return a reference to local
  • don’t return T&& and std::move(local).

Lambda

use lambda to be more expressive

1
2
3
4
5
std::sort(myStrVec.begin(),myStrVec.end(),
[](const std::string& f, const std::string& s){
return f.size() < s.size();
}// a comparator function
)

Capturing

  • capture by reference => used locally
  • don’t capture by reference => nonlocally( returned, stored on the heap, passed to another thread)
    correct version of thread lambda
    1
    2
    3
    4
    std::string("C++ 11");

    std::thread thr([&str] {std::cout<< str << '\n';})
    thr.join(); //wait the thread to be joined

Use default argument over overloading. (DRY)

1
void print(const string& s, format f = {});

Class

Rules

  • orginize related data into structures.(Point, Info, …)
  • struct: default public; class: default private;
  • use class => logical relationship, and invariant between members.
  • checked the invariant in constructor.
  • differentiate the public and private methods. Use public methods as interface. Minimize the exposure.
  • Only if a member function need access to private section of a class, make it a member.
  • Write helper function in the same namespace with the class.
    1
    2
    3
    4
    5
    namespace Chrono{
    class Date{ ... };
    bool operator == (Date, Date);
    Date nextDate(Date);
    }

Constructor, assignments and destructors

Big Six:

  1. X()
  2. X(const X&): copy constructor
  3. operator = (const X&):copy assignment
  4. X(X&&): move constructor
  5. operator = (X&&): move assignment
  6. ~X()
  • use default operation if you can.
  • TLDR: define all big six or all delete them. Reasons:
    • when you define destructor,copy cons, conpy assign the compiler will not generate move cons and move assign. and fall back to a copy assign.
    • when you define a mov cons or mov assign, compiler will delete the copy cons and copy assign.
  • Be carefully with shadow copy.

Constructor

  • a constructor should generate a fully initialized object. (don’t use init(), use delegate constrution)
  • use member initializers. => lower code repetition.