Data Member Initialization

  • The compiler generated default constructor
    • which initialize all data members of class (user-defined) type, but not the data member of fundamental type
  • The best way is to define your own default constructor, properly initialize all data member
  • Define and initialize member variables in the order of member declaration
    • 在下面的例子中,i(42), s("Hello World"), ps(nullptr) 的順序不能變,因為他們被 declare 的順序就是如此
// bad
struct Widget {
  int i;
  std::string s;
  int* ps;
};
// better
#include <string>
#include <iostream>
 
struct Widget {
  int i;
  std::string s;
  int* ps;
 
  Widget():
    i(42),
    s("Hello World"),
    ps(nullptr)
  {}
};
 
int main() {
  Widget w{};
  return 0;
}

若有多個不同的 constructor,可以使用 value initialization ,這樣每個 constructor 只需要專注在自己要 initialize 的部分即可,因為其他的都有 default。如此一來就避免了 duplication(兩個 constructor initialize 相同的東西)。

// Bad! Duplication!
Widget():
    i(42),
    s("Hello World"),
    ps(nullptr)
{}
 
Widget(int j):
	i(j),
    s("Hello World"),
    ps(nullptr)
{}
#include <string>
#include <iostream>
 
// even better
struct Widget {
  Widget(){}
  Widget(int j):
    i(j)
  {}
 
  int i{42};
  std::string s{"Hello World"};
  int* ps{};
};
 
int main() {
  Widget w{};
  return 0;
}

Constructor with explicit keyword

  • To prevent implicit conversion

Const and Reference to Data Member

  • compiler 不知道該如何 initialize const data member
  • reference 同理(reference to what?)
struct Widget {
    int const i;
    double& d;
};
 
int main() {
    Widget w;
}
/*
note: 'Widget::Widget()' is implicitly deleted because the default definition would be ill-formed:x86-64 gcc 13.2 #Executor 1
error: uninitialized const member in 'struct Widget'x86-64 gcc 13.2 #Executor 1
error: uninitialized reference member in 'struct Widget'x86-64 gcc 13.2 #Executor 1
*/

對於 reference 而言,reference members can be stored as pointers:

struct Widget {
private:
  double* pd_;
public:
  Widget(double& d):
    pd_(&d)
  {}
  double& get() noexcept {return *pd_;};
};
 
int main() {
  double d1 = 2.34;
  Widget w{d1};
  return 0;
}

Accessibility v.s. Visibility

對於 compiler 來說,不論 private or public 都是可見的,所以會選擇要執行 void doSomething(int d);,然後才發現該 member function 為 private(accessibility),接著給出 error。

struct Widget {
private:
  void doSomething(double d);
public:
  void doSomething(int d);
};
 
int main() {
  Widget w{};
  w.doSomething(1.0);
  return 0;
}
/*
<source>:10:16: error: 'void Widget::doSomething(double)' is private within this context
   10 |   w.doSomething(1.0);
      |   ~~~~~~~~~~~~~^~~~~
*/

Operator Overload

#include <iostream>
using namespace std;
 
class Box {
private:
    int width_;
    int height_;
public:
    Box();
    Box(int width, int height);
    Box& operator+(const Box& b);
    operator int() const;
    friend ostream & operator<<(ostream& os, const Box& b);
};
 
Box::Box(int width, int height) {
    width_ = width;
    height_ = height;
}
 
Box& Box::operator+(const Box& b) {
    this->width_ += b.width_;
    this->height_ += b.height_;
    return *this;
}
 
Box::operator int() const {
    return width_ + height_;
}
 
ostream & operator<<(ostream& os, const Box& b) {
    os << "width: " << b.width_ << " height: " << b.height_ << endl;
    return os;
}
 
 
// class Derived {
// private:
 
// public:
// };
 
 
/*
Conversion takes place for member function arguments, not for member function invokers.
The lesson here is that defining addition as a friend makes it easier for a 
program to accommodate automatic type conversions.The reason is that both 
operands become function arguments, so function prototyping comes into play 
for both operands.
 
The class may be implicityly convert to integer, and conversion happens only when it is function 
arguments, that's why friend function is better in this case
*/
 
int main() {
    
    Box box1 = {3, 5};
    Box box2 = {7, 9};
    int b_int = box1;
    cout << b_int << endl;
 
    // box1 = b_int + box2; 
    // error: no match for 'operator=' (operand types are 'Box' and 'int') box1 = b_int + box2;
    box1 = box1 + box2;
    cout << box1 << " " << box2 << endl;
    return 0;
}

Rule of Three & Rule of Five

對於 String class 而言,如果不 implement copy constructor,s1 = s2 就會造成兩個 String 具有相同的 char* str (shallow copy),destructor 就會 delete 同一個 pointer 兩次,造成 error。

#include <iostream>
#include <cstring>
 
class String {
private:
    char *str;
    int len;
    static const int CINLIMIT = 80;
public:
    String(); // default constructor
    String(const char *s); // constructor
    ~String(); // destructor
    String(const String& s); // copy constructor
    int length() const {return len;};
 
    String& operator=(const String& s); // copy assignment operator
    String& operator=(const char *s); // copy assignment operator
    
    char& operator[](int i);
    const char& operator[](int i) const;
 
    operator int() const {return len;};
 
    friend bool operator<(const String& s1, const String& s2);
    friend bool operator>(const String& s1, const String& s2);
    friend bool operator==(const String& s1, const String& s2);
    friend std::ostream& operator<<(std::ostream& os, const String& s);
    friend std::istream& operator>>(std::istream& is, const String& s);
};
 
String::String() {
    len = 0;
    str = nullptr;
}
 
String::String(const char* s) {
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
}
 
String::~String() {
    len = 0;
    delete [] str;
}
 
String::String(const String& s) {
    len = s.len;
    str = new char[len + 1];
    std::strcpy(str, s.str);
}
 
 
String& String::operator=(const String& s) {
    if(this == &s) {
        return *this;
    }
 
    delete [] str;
    len = s.len;
    str = new char[len + 1];
    std::strcpy(str, s.str);
    return *this;
 
}
 
String& String::operator=(const char *s) {
    delete [] str;
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    return *this;
}
 
char& String::operator[](int i) {
    return str[i];
}
 
// read-only access
const char& String::operator[](int i) const {
    return str[i];
}
 
// operator int() const;
 
bool operator<(const String& s1, const String& s2) {
    return (std::strcmp(s1.str, s2.str) < 0);
}
 
bool operator>(const String& s1, const String& s2) {
    return s2 < s1;
}
 
bool operator==(const String& s1, const String& s2) {
    return (std::strcmp(s1.str, s2.str) == 0);
}
 
std::ostream& operator<<(std::ostream& os, const String& s) {
    os << s.str;
    return os;
}
 
int main() {
    String s1 = "Jimmy Lin";
    String s2 = s1;
    std::cout << s1 << " " << s2 << std::endl;
 
    
    return 0;
}

下列的 SString class 當中,使用 unique_ptr 當作 data structure,因為 unique_ptr 自己知道如何 move 以及釋放記憶體,所以我們不需要 implement move constructor、 move assignment operator 以及 destructor,但是因為 unique_ptr 不知道該如何 copy 自己,所以我們要 implement copy constructor & copy assignment operator,但是又因為 Rule of Five,因此 compiler 就不會提供 default move constructor、 move assignment operator 以及 destructor 給我們了,解決的方式是使用 ~SString() = default; // default destructor 去告訴 compiler 我們想要使用 default。

lesson learned:如果能夠全部都使用 default(Rule of Zero)那是最好,不需要自己 implement(you probably will mess up something)。如果不行,就按照 rule of three or five 搭配 default keyword。

#include <iostream>
#include <cstring>
#include <memory>
 
class SString {
private:
    std::unique_ptr<char[]> str;
public:
 
    ~SString() = default; // default destructor
    // constructor
    SString(const char * cp) : str(new char[std::strlen(cp) + 1]) {
        std::strcpy(str.get(), cp);
    }
    // copy constructor
    SString(const SString& s): str(new char[std::strlen(s.str.get()) + 1]) {
        std::strcpy(str.get(), s.str.get());
    }
 
    // copy assignment operator
    SString& operator=(const SString& s) {
        str.reset(new char[std::strlen(s.str.get()) + 1]);
        std::strcpy(str.get(), s.str.get());
        return *this;
    }
    // move constructor
    SString(SString&& s) = default;
    // move assignment operator
    SString& operator=(SString&& s) = default;
    
    char& operator[](int i) {
        return str.get()[i];
    }
 
    friend std::ostream& operator<<(std::ostream& os, const SString& s) {
        os << s.str.get();
        return os;
    }
};
 
 
int main() {
    SString s1 = "Jimmy Lin";
    SString s2 = s1;
    s2[2] = 'g';
 
    std::cout << s1 << std::endl;
    std::cout << s2 << std::endl;
 
    return 0;
}

Multiple Inheritance

Multiple Inheritance can result in ambiguous function calls. For example, a BadDude class could inherit two quite different Draw() methods from a Gunslinger class and a PokerPlayer class.

Design Principles

  • Single-Responsibility Principle
    • Separation of Concerns
    • High cohesion / low coupling
    • Orthogonality
  • Open-Closed Principle
    • prefer design by simplified the extension by types or operations
  • Don’t Repeat Yourself(DRY)
  • strategy pattern