1
2
3
4
5
cl /I <include path>
link /LIBPATH:<library path> /OUT:<output file>

cl /I "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include" /c hello.cpp
link /LIBPATH:"C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\lib" /LIBPATH:"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Lib" /OUT:hello.exe hello.obj

library path 加 C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Lib 主要為了 kernel32.lib

不過很少直接用 cl,通常都是用 Visual Studio 整套 IDE。

self assignment:object 被 assign 給自己。

class member 有 pointer 時寫 operator=() 要小心處理 self assignment。如果先把自己原本的 member delete 掉,等同把 rhs 的 member 也 delete 掉,assign 後會得到爛掉的 pointer。

1
2
3
4
5
6
7
8
9
10
11
12
Foo& Foo::operator=(const Foo& rhs)
{
Bitmap* pOrig = pb; // pb is member pointer in Widget

if (rhs.pb != NULL)
pb = new Bitmap(*rhs.pb);
else
pb = NULL;

delete pOrig;
return *this;
}

這做法可以處理 member pointer 但會讓 member pointer 指的位置經過 self assignment 後變得不同,另一種做法是檢查 this 是否跟 &rhs 相同,不同時才真的做 copy。

Ref

  • 《Effective C++》

symbol 的 definition 可分為 strong symbol 跟 weak symbol。C/C++ 的 compiler 預設 function 及有初始化的 global variable 為 strong symbol,未初始化的 global variable 為 weak symbol。strong & weak symbol 跟處理 symbol 重複定義有關:

  1. 不允許 strong symbol 重複定義,有的話會 link error。
  2. 如果一個 symbol 在某個 object file 中是 strong symbol,其他都是 weak symbol,選 strong symbol。
  3. 如果都是 weak symbol,選 type size 最大的。

Usage

GCC 中可用 __attribute__((weak)) 來定義一個 strong symbol 為 weak symbol:

weaksym.cpp
1
__attribute__((weak)) int x = 2;	// weak symbol
main.cpp
1
2
3
4
5
6
7
8
#include <iostream>

int x = 123; // strong symbol

int main() {
std::cout << x << endl; // result is 123
return 0;
}

weak symbol 可以在 link time 置換 function。一開始給個預設 implementation 並設為 weak symbol,使用者可以寫 function 編成 object file 去 link。由於使用者寫的是 strong symbol 會蓋掉原本的 default implementation,達到 link 階段換 implementation。

weakfoo.cpp
1
2
3
4
5
6
7
8
9
10
#include <iostream>

extern void foo() __attribute__ ((weak));

void foo() { std::cout << "default foo" << endl; }

int main() {
foo();
return 0;
}
foo.cpp
1
2
3
#include <iostream>

void foo() { std::cout << "custom foo" << endl; }
1
2
3
4
5
6
7
8
> g++ -c weakfoo.cpp -o weakfoo.o
> g++ -c foo.cpp -o foo.o
> g++ weakfoo.o
> ./a.out
default foo
> g++ weakfoo.o foo.o
> ./a.out
custom foo

Ref

link DLL 分成 implicit link 及 explicit link。

DLL library 需要 export 出 symbol,使用 DLL 的程式則需要 import symbol。VC 裡透過 __declspec(export) 來標示要 export 的 symbol,以及 __declspec(import) 標示要從外面 import 的 symbol。如果要讓 C++ 的 symbol 跟 C 相容,需要加 extern "C"(不做 C++ 名稱修飾)。

使用 library 的程式需要:

  • compile 時需要 library export 的 symbol 的 header file
  • 需 link library 的 .lib.lib 會在 build DLL 時一起 build 出來
  • 執行時需 .dll

build DLL 產生的 .lib 跟 static library 的不一樣,DLL 的 .lib 只是告訴使用的程式去哪找到 DLL,不會包含實際功能,所以檔案比較小。使用 implicitly link DLL 的程式必須要在 load 時可以找到 DLL,否則會跳錯誤訊息而且無法繼續執行。

作為 library 的 project 需在 VC 的 project properties→General→Configuration Type 設為 Dynamic Library (.dll)、Target Extension 設為 .dll。另外,如果 code 實際上沒有 export symbol,build DLL 時不會生出 .lib

Sample

Foo library

compile 時需 define FOO_DLL_EXPORTS

Foo.h
1
2
3
4
5
6
7
8
9
10
11
12
#ifndef FOO_H
#define FOO_H

#ifdef FOO_DLL_EXPORTS
#define FOO_API __declspec(dllexport)
#else
#define FOO_API __declspec(dllimport)
#endif

FOO_API int Add(int a, int b);
extern "C" FOO_API int Sub(int a, int b);
#endif
Foo.cpp
1
2
3
#include "Foo.h"
int Add(int a, int b) { return (a + b); }
int Sub(int a, int b) { return (a - b); }

dumpbin 要從 VC 的 command prompt 才叫得出來。

export symbol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\tmp\FooLibrary\Debug>dumpbin /EXPORTS FooLibrary.dll
Dump of file FooLibrary.dll

File Type: DLL

Section contains the following exports for FooLibrary.dll

00000000 characteristics
56507D3B time date stamp Sat Nov 21 22:18:35 2015
0.00 version
1 ordinal base
2 number of functions
2 number of names

ordinal hint RVA name

1 0 0001107D ?Add@@YAHHH@Z = @ILT+120(?Add@@YAHHH@Z)
2 1 000110FA Sub = @ILT+245(_Sub)

Test program

project 的 include file 中須包含 Foo.h,link library 需有 FooLibrary.lib,執行檔旁則需放 FooLibrary.dll

main.cpp
1
2
3
4
5
6
7
#include <iostream>
#include "Foo.h"
int main()
{
std::cout << Add(1, 2) << ", " << Sub(1, 2) << endl;
return 0;
}

在 runtime 時才 load DLL。因為 runtime 才 load,即使 load DLL 失敗也可以在程式裡處理錯誤並繼續執行下去。

使用 library 的程式需要:

  • call LoadLibrary() load DLL
  • call GetProcAddress() 取得想要的 function 的 address
  • 用完 library 需 call FreeLibrary()
  • 程式 compile 時不一定需要 library 的 header file(但需要知道要 call 的 function 的 prototype),link 時不需要 .lib,僅在執行時需要 .dll

GetProcAddress() 需指定的 function name 是 library export 出來的 symbol,不是 library source code 裡的 function name。經過 C++ 名稱修飾,需要指定的 function name 會變得難以理解,這種 interface 應該沒人想用。除了 __declspec(dllexport) 外,export function symbol 的另一個做法是使用 .def 模組定義檔來宣告名稱。實際上是指定 alias 給原本的 symbol。

Sample load DLL

library source code 同上。

有加 extern "C"Sub() 因為沒經過 C++ 名稱修飾,所以能直接用 function name,但 Add() 就得寫出 C++ 修飾後的 symbol name 才拿得到 function pointer。

main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <windows.h>

typedef int(*pfn)(int, int);

int main()
{
HINSTANCE dllHandle = LoadLibrary("FooLibrary.dll");

if (dllHandle != NULL)
{
// Get address of function
pfn pSubFunc = (pfn)GetProcAddress(dllHandle, "Sub");

if (!pSubFunc)
{
std::cout << "Load Sub() fail" << std::endl; // handle the error
}
else
{
std::cout << pSubFunc(2, 3) << std::endl; // call the function
}

pfn pAddFunc = (pfn)GetProcAddress(dllHandle, "?Add@@YAHHH@Z");

if (!pAddFunc)
{
std::cout << "Load Add() fail" << std::endl;
}
else
{
std::cout << pAddFunc(2, 3) << std::endl;
}

FreeLibrary(dllHandle);
}
else
{
std::cout << "Load FooLibrary.dll fail" << std::endl;
}

return 0;
}

Sample 模組定義檔

刪掉 Foo.h 裡的 __declspec(dllexport)

Foo.def
1
2
3
4
LIBRARY FooLibrary
EXPORTS
Add
Sub
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
D:\tmp\FooLibrary\Debug>dumpbin /EXPORTS FooLibrary.dll
Dump of file FooLibrary.dll

File Type: DLL

Section contains the following exports for FooLibrary.dll

00000000 characteristics
56516FE9 time date stamp Sun Nov 22 15:34:01 2015
0.00 version
1 ordinal base
2 number of functions
2 number of names

ordinal hint RVA name

1 0 0001107D Add = @ILT+120(?Add@@YAHHH@Z)
2 1 000110FA Sub = @ILT+245(_Sub)

main.cppGetProcAddress() 可以直接寫 AddSub

其實這是小實驗筆記吧…

Ref

最近看了些 code,發現不同情況下有不同 trace code 的方式,來 murmur 一下。

通常拿到一份 code 會先看它主要 component 的結構。如果有 UI 會先大概了解有哪些 component、分別叫什麼、彼此階層關係是什麼,例如哪個 container 裡放著什麼之類的。如果是網頁會先看資料夾結構是一般自己寫的還是用 framework。之後依照要做的事情不同而有不同的 trace 方式。

第一種,debug 或者找特定功能。

意識到這種 trace code 方式應該是大學在計中打工的時候,那時候想將 Wordpress 跟一個系統做簡單的整合連結,所以要找 Wordpress 裡相對應的功能。工作後 debug 也常常是用這種方式在看 code。

  • 如果有 UI 操作,從 UI 操作 trigger 的地方開始一路往下看。
  • 簡單的東西可能只需要找到特定 function,改一改或加一加功能就好。
  • 稍微複雜一點就得看懂整條路在幹嘛。

這方式是單看程式裡某一條路徑、某一段特定邏輯,除非發現是架構上的 bug 才會再往外擴。要是對那份 code 很熟,是也不用從最開始往下找啦……

第二種,想知道程式整體結構或運作之類的。

想全面但不深入細節的了解結構跟 high level 的邏輯概念。著重架構及概念,會看大致的流程邏輯,但不會細看每個 function 怎麼實作。這是最近演化(?)出來的方式。

  • 找最主要的 component 當起點,通常那 project 叫什麼主要 component 就叫什麼,找不到就從 main() 開始。
  • 看 class name 猜用途猜猜樂
    • 看名字看不出來的就看 public function 來了解這 class 提供什麼功能
    • function name 還是看不出來,找其他地方如何使用或者快速掃一下實作
    • 有時候會遇到「這個 class 就是這堆功能的集合,我也看不出來這名字跟這堆功能有啥關係」的狀況就是……
  • 需要知道某些流程的時候
    • 以類似第一種做法順著流程邏輯看,但只看概略,不細看實作。
    • 偶爾會看點實作,但比較像用大筆刷過去……第一種方式看實作比較像要刻字那樣精雕細琢…….我到底在寫什麼…Orz…
  • 了解各 object 的關係,通常看 member。
    • 如果是 pointer 要注意是自己生的還是別人傳進來的。
  • 遇到某些關鍵字,例如 XXXFactory、XXXObserver,直接套用已知概念。
    • 只注意誰跟誰有這類的關係,不細看如何實作這些關係。當然也有人家有關鍵字但我不會那概念就沒東西套的狀況……XD
  • 遇見某些常見寫法,直接套用那種寫法的概念。
    • 例如 select() 常常就是一個 while loop、塞一塞 fdset、call select()、後面依照 fdset 做事。某些 event loop 做法也有相似性。

我覺得如果遇到關鍵字跟常見做法可以直接套用已知觀念,相對來說就會快很多,因為不需要特別再看實作去理解這部分在做什麼。有時候需要看實作是因為拼湊其他線索後還是不知道那段在幹嘛,只好透過實作細節重新抽象化成概念。

至於如何找 code?find all 與 grep --color -nr * 萬歲!(欸)

PS:寫這個是想知道自己怎麼 trace code 的,但怎麼寫一寫好像還是有點像要心領神會的難以言喻……Orz……

API 分很多種 style,各有優缺跟取捨。

  • C style
  • Object Oriented C++ style
  • Template style
  • Data driven style

這種分類方式是以 C/C++ 系列區分,但我覺得在其他語言方面也有類似概念。

C style

一般 C function 們,通常會用 struct 跟一些命名規則來區別不同功能跟 component 等等。

類似的東西:PHP 那些 mysql_ 開頭的 function 們。

讓 C++ 可以 call C API

有時候使用者是用 C++ 開發,而 C style 的 API 希望能給這樣的使用者使用。在此狀況下,C style API 需要:

  1. C API 可以用 C++ compiler 編過
  2. extern "C" 處理 C++ 與 C 之間 linkage 的問題
    同樣的 function 在 C compiler 跟 C++ 的 compiler 產生的 object file 中會以不同方式呈現,例如 C++ 有 name decoration,但 C 沒有。因此 C API 得用 extern “C” 包起來,讓 C++ compiler 知道這段應該要用 C style 的 link 方式。
1
2
3
4
5
6
7
8
9
#ifdef __cplusplus
extern "C" {
#endif

// C API declaration

#ifdef __cplusplus
}
#endif

實例:C++ uses C library example

Object Oriented C++ style

以物件導向為基礎的寫法。

類似的東西:PHP 的 mysqli 物件。

Template style

以 template 於 compile time 達到 static polymorphism──相同 interface 可支援多種 type,例如 STL。

相對物件導向的多型是在 runtime 做,使用 template 有較高的執行效率,但 compile 出來的檔案會比較大。因此,相對 code 大小較著重執行效能時可用 template,反之可用物件導向。

缺點之一是 template 的定義得放在 header,導致使用的程式需一起 compile template 的 header,但仍有些方式可以將 template 定義藏在 .cpp。另外,template 的 compile error 訊息很複雜,無論自己開發有什麼問題或者使用者使用時有何問題,複雜的 error message 讓人蠻頭大的。被害過…但至今還是沒搞懂…(欸)

Data driven style

interface 提供非常 general 的 function,實際上做什麼事由 input 決定。interface 大概像這樣:

Result DoCommand(string cmd, ArgList args)

Command() 內部 implement 會依據 cmd 決定要做什麼,args 則是對應 command 的參數,Result 則是回傳值。

例如 web service,使用者丟想做的命令跟參數給 web service,web service 再依據命令及參數回應。

由於 interface 非常 general,需要相對應的 general 的參數型態來傳這些參數,像上面的 Result。在不強調 data type 的語言(weakly type language)像 python 中很好處理,但在 C++ 裡就要自己寫或用其他 library 提供的了,如 Qt 的 QVariant。

好處是 API 修改非常彈性,幾乎不需要動到 interface。壞處是單從 interface 看不出 API 提供的功能,因為太 general 了,這時候只能靠文件。

C compiler 編出來的 object file 可以跟 C++ compiler 編出來的 object file link 在一起。

假設 Linux 環境下有一個 object file foo.o 是 C compiler 編出來的,可以將 foo.o 看作一 C library,而 main.cpp 是由 C++ 實作並且使用 foo.o 裡的 function,因此 main.cpp 會 include 內含 function declaration 的 foo.h。C++ compiler 在一般狀況下會將 foo.h 內的 function 當作 C++ function 以 C++ 的規則處理。

由於 foo.o 是 C compiler 編出來的,其對 function 的處理方式不像 C++ 會額外加修飾,使得 link 時 main.ofoo.o 中 symbol 對不起來而產生 undefined reference 錯誤。在 foo.h 中加上 extern "C" 是告訴 C++ compiler 要把這段 function declaration 當作 C function 處理,也就是不做 name decoration,之後 link symbol 才對得起來。實例如下:

foo.h
1
2
3
4
5
6
7
8
9
10
// foo.h
#ifdef __cplusplus
extern "C" {
#endif

void foo();

#ifdef __cplusplus
}
#endif
foo.c
1
2
3
4
5
6
7
8
// foo.c
#include <stdio.h>
#include "foo.h"

void foo()
{
printf("C foo()!\n");
}
main.cpp
1
2
3
4
5
6
7
8
9
10
// main.cpp
#include <iostream>
#include "foo.h"

int main()
{
std::cout << "C++ main" << endl;
foo();
return 0;
}

foo.o 的 symbol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> gcc -c foo.c
> readelf -s foo.o

Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS foo.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 8
8: 00000000 0 SECTION LOCAL DEFAULT 6
9: 00000000 20 FUNC GLOBAL DEFAULT 1 foo
10: 00000000 0 NOTYPE GLOBAL DEFAULT UND puts

一般狀況的 main.o symbol(重點在 18):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
> g++ -c main.cpp
> readelf -s main.o

Symbol table '.symtab' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS main.cpp
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 1 OBJECT LOCAL DEFAULT 4 _ZStL8__ioinit
6: 00000000 0 SECTION LOCAL DEFAULT 5
7: 00000039 64 FUNC LOCAL DEFAULT 1 _Z41__static_initializati
8: 00000079 28 FUNC LOCAL DEFAULT 1 _GLOBAL__sub_I_main
9: 00000000 0 SECTION LOCAL DEFAULT 6
10: 00000000 0 SECTION LOCAL DEFAULT 9
11: 00000000 0 SECTION LOCAL DEFAULT 10
12: 00000000 0 SECTION LOCAL DEFAULT 8
13: 00000000 57 FUNC GLOBAL DEFAULT 1 main
14: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZSt4cout
15: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZStlsISt11char_traitsIcE
16: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZSt4endlIcSt11char_trait
17: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSolsEPFRSoS_E
18: 00000000 0 NOTYPE GLOBAL DEFAULT UND _Z3foov
19: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4InitC1Ev
20: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4InitD1Ev
21: 00000000 0 NOTYPE GLOBAL DEFAULT UND __dso_handle
22: 00000000 0 NOTYPE GLOBAL DEFAULT UND __cxa_atexit

foo.hextern "C"main.o symbol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>readelf -s main.o

Symbol table '.symtab' contains 23 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS main.cpp
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 1 OBJECT LOCAL DEFAULT 4 _ZStL8__ioinit
6: 00000000 0 SECTION LOCAL DEFAULT 5
7: 00000039 64 FUNC LOCAL DEFAULT 1 _Z41__static_initializati
8: 00000079 28 FUNC LOCAL DEFAULT 1 _GLOBAL__sub_I_main
9: 00000000 0 SECTION LOCAL DEFAULT 6
10: 00000000 0 SECTION LOCAL DEFAULT 9
11: 00000000 0 SECTION LOCAL DEFAULT 10
12: 00000000 0 SECTION LOCAL DEFAULT 8
13: 00000000 57 FUNC GLOBAL DEFAULT 1 main
14: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZSt4cout
15: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZStlsISt11char_traitsIcE
16: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZSt4endlIcSt11char_trait
17: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSolsEPFRSoS_E
18: 00000000 0 NOTYPE GLOBAL DEFAULT UND foo
19: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4InitC1Ev
20: 00000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4InitD1Ev
21: 00000000 0 NOTYPE GLOBAL DEFAULT UND __dso_handle
22: 00000000 0 NOTYPE GLOBAL DEFAULT UND __cxa_atexit

盡量不要讓看過的書像船過水無痕,嘗試用自己的話做小結。

這章的重點在:What are the basic qualities of a good API?

Model the problem domain

API 是用來解決某個問題的,問題可大可小。

API 要能對問題提供抽象化的概念,並且能夠將這抽象概念以 interface 表達出來。以 C++ 來說,我的理解是用 class name 以及 function name 表達抽象概念,也就是使用 API 的人應該要能從 class 及 function 看出抽象概念及使用邏輯。

一個問題沒有絕對正確的抽象化方式,重點是 API 要保有某種一致性及邏輯。

如果用物件導向來做 modeling 就會用 object modeling,也就是會定出 object、每個 object 做什麼、object 之間的關係與互動。一個 class 應該要定義為「做什麼」(what)而不是「怎麼做」(how)。

Hide implementation details

隱藏所有實作細節。如果沒藏好,以後要改實作,使用者可能跟著改到崩潰然後就不想用你的 API 了。(無誤)

interface 訂得好,底下怎麼搞都沒關係,改 interface 比較會影響到使用者。

隱藏實作細節的方法:

  • Declaration & Definition
    盡量在 header 只寫 declaration,implementation 放在 cpp。
  • Encapsulation
    • 請把 member 藏起來,外面需要它們的話使用 getter & setter。
    • 隱藏 class 內部才使用的 implementation method 們,API 使用者不需要知道 API 實際上如何實作出功能的。
    • 隱藏 class 內部才使用的 implementation class 們。
    • 所謂隱藏就是設成 private。

PS:這裡說的使用者是指使用 API 的人,通常是其他 programmer 或自己,不是使用軟體的一般使用者。

Minimally complete

API 功能要完整,可以滿足使用者需要的所有功能,要盡可能小但無法再更小了。

要小心 virtual function 可能會公開過多 function 給使用者。不過我還沒很懂這意思…

Easy to use

看到這裡總覺得在軟體設計上到處都有這句話,只是層次不太一樣。在 application 層級上,easy to use 希望的是一般使用者的好用。而在 API 層級上則是針對 API 使用者──通常是 programmer。

  • Discoverable
    使用者光看 API 就能自己心領神會、找出如何使用。
  • Difficult to misuse
    難以誤用,舉例:傳三個 enum Year、Month、Day 當參數比傳三個 int 不容易誤用。《Effective C++》好像說過同樣的話…
  • Consistent
    API 設計的一致性
  • Orthogonal
    • method 之間沒 side effect,例如 call 改變某屬性的 method 不會動到其他屬性。
    • 修改 API 的部分實作也不會影響到其他部分。
    • 做法:保持一種資訊只有一個地方有,不要到處 copy-paste code。盡量封裝,避免一個變數到處都可以 access 以致改了 A 可能就動到 B 的行為。
  • Resource allocation
    • 用 smart pointer 管理 memory。
    • 其他種類的 resource 也可以用 class 加以管理,object 的 construct 是 allocate resource,destruct 則是 release resource,通常稱為 RAII(Resource Acquisition Is Initialization)。舉例:Qt 的 QMutexLocker。
  • Platform independent
    避免在公開的 header 用針對特定平台的 #if#ifdef,例如 #ifdef _WIN32

鬆散耦合

跟 OO 的原則一樣,class 之間、module 之間不要黏太緊。

簡單看兩個 component A 跟 B 黏得緊不緊的方式是看改了 A 後 B 會不會改很多、A 看得到 B 多少東西(如只看得到 public function 還是也看得到 private member 等等)。

要避免兩個 component 互相依賴變成 dependency cycle,不然想用 A 就一定要有 B,但邏輯上可能根本不需要 B,看起來就很怪。

這邊提兩種鬆散耦合的方法:Manager class 跟事件通知。

Manager class

Manager class 會擁有並管理某些 class,例如與 output 有關的 class。

如果有一個 Manager class 管理 N 個 output class,其他 M 個 需要使用 output class 的 class 可以不需要跟所有 output class 都有關係(這樣會有 N * M 個關聯),大家可以只跟 Manager class 有關係(只剩 N + M 個關聯),從而降低 outpu class 與其他 class 間的耦合。

原來 Manager class 還有這種功能…

事件通知

在「某件事發生時需要通知其他人」的情境下,有以下幾種方式可以降低通知者與被通知者的耦合:

  • callback
    C 寫法,使用 function pointer,也可以有很多 callback。
  • observer
    Observer Pattern
  • notification
    不針對特定事件,比較像整個系統的機制,例如 signal-slot。

這些方法可以讓通知方與被通知方彼此不需要知道對方是誰,否則雙方得知道對方是誰才能通知,而當兩邊各有很多 class 的時候,那根本是場災難……

相關 note

Murmur

老覺得跟 OO 的原則很類似,很多概念都有在其他地方看過。不過現在再看,似乎更能配合實際經驗加以理解。

像 naming、argument 順序、standard pattern 的使用、memory model、exception 的使用、error handling 等等,如果設計上有一致性,使用者較容易使用。保有一致性時,使用者可以輕易透過原本的理解認知推測新功能或其他部分如何使用。

在設計 C++ API 上,如果跟 standard library 保有相同的模式與規則也會讓 API 易於使用,畢竟大家都會用 standard library 啊!

雖然這篇標題是說 API 的設計,但小至 class 跟 function 也是有類似的觀念啊~

舉個 argument 不一致的例子:fscanf()fgets(),它們分別長這樣:

int fscanf(FILE* stream, const char* format, ...);
char* fgets(char* str, int num, FILE* stream);

兩個類似的 function,FILE* stream 參數卻是一個在第一個、一個在最後一個,讓我太久沒用就得回頭瞄一下文件……

API 的功能可以是比較 atomic、比較基本的,讓外部使用這些基本功能組合出它想要的東西。也可以是 API 本身即組合一些基本功能、提供較方便的功能給外部使用。這兩種特性雖然互相衝突,但都是作為 API 希望滿足的。

滿足這兩種特性的方式是將僅提供基本功能的 core API 及較方便的 API 分開。可能是以 class 或檔案區隔,甚至直接拆成不同 library。重點是較方便的 API 是使用 core API 的 public interface,就像在 core API 外再包一層,而不會 access 到 core API 的內部結構。

如此使用者就能有方便使用的 API 也能在想自行組合功能的時候使用進階的 core API。這概念類似於 UI 設計上常有的「進階」按鈕──平常設定頁只會有基本設定,按進階按鈕後才會顯示更細部的設定。