顯示具有 Cpp 標籤的文章。 顯示所有文章
顯示具有 Cpp 標籤的文章。 顯示所有文章

2017年12月23日 星期六

Type driven development -- by C++

Let's start from some code. And seems will be only code in this article.
// Compile: clang++ main.cc
#include <iostream>

template <int x, int y> class Matrix {
  // We don't care how to implement at here
public:
  std::string print() { return std::string("Matrix"); }
};

template <int x, int y> Matrix<x, y> Add(Matrix<x, y> a, Matrix<x, y> b) {
  return a; // Just help compile can run it.
}

int main() {
  std::cout << Add(Matrix<2, 2>(), Matrix<2, 2>()).print() << std::endl;
  // This line never pass, interesting part.
  std::cout << Add(Matrix<3, 2>(), Matrix<2, 3>()).print() << std::endl;
}
Ok, some code be there, why I want to talk about these code?
Few weeks ago, I study Idris and it's core concept: Type-Driven-Development.
But what is TDD(T is Not test at here)?

Matrix can show this concept clearly. Because we need some meta to make sure we are adding correctness Matrix together.
We don't want something like [0 0] + [1 0 3] can work, because Matrix can't be that.
So what will we do at first? Every programmer will check it(I thought, hope I am correct). And most of them will check it at: runtime. But runtime checking is danger. If I could, I always trying compile-time checking, because the chance that can be find out by editor is very big, almost 100%. But how to do that?

In C++, template help we checking at compile-time.
And almost no other language can template integer as template parameter. In Java, we have generic only. And a lot language only have generic too.
But maybe some people can't understand idris, so let's use C++.

The point is: when we need Matrix add. Only those Matrix with correct X, Y can add together.
With template check, second Add always can't pass compile.
Hope you already got the point of TDD.
That is  define type for certain usage, you can get the help from Type System.
It can limit error into a narrow part.
Thanks for read.

2017年11月18日 星期六

Swift -- extension skill, impl policy base design

經過幾個禮拜的思考,我認爲jon hoffman先生說得有理,我們應該傾向簡單的設計,而Swift不是C++,因此不應該試圖模仿
回到Poilcy的設計上,我們最初的期望是:在編譯期選擇型別,並且透過隱式約束,而不是顯式
先看看C++的案例
struct SQLServer {
    void connect();
    // ...
}
struct MySQL {
    void connect();
    // ...
}
class DB<DBImpl> {
public:
    void connect() {
        DBImpl().connect();
    }
}
我們建構泛型DBImpl的實例並且呼叫其方法,我們知道C++的樣板會在"使用"時展開並檢查
因此我們不會有執行期(runtime)成本,且使用時檢查此點非常重要,這是我們之後會在Swift遇到的難題

那麼我們要如何在Swift中實現一樣的能力呢?
首先我們要了解到Swift並非在使用時檢查,而是宣告時檢查,這只得剛才的程式寫法完全無法編譯通過:首先編譯器會告訴你DBImpl沒有建構式(在C++,如果你沒有宣告建構式,編譯器會幫你寫一個),接著編譯器認為DBImpl沒有connect方法
我最開始的解決方案是利用constraint讓編譯器得到這些資訊
protocol DBPolicy {
    init()
    func connect()
}
class SQLServer : DBPolicy {
    required init() { ... }
    func connect() { ... }
    // ...
}
class MySQL : DBPolicy {
    required init() { ... }
    func connect() { ... }
    // ...
}
class DB<DBImpl: DBPolicy> {
    func connect() {
        DBImpl().connect()
    }
}
可是這樣一來,程式就變得沒有彈性,而且這樣搞,寫多型不就好了(我們只享受到一點效能優化,但是沒有得到最重要的彈性)

接下來我學習到where clauses這個特性,我終於找到了正確的實作方式
class SQLServer {
    required init() { ... }
    func connect() { ... }
    // ...
}
class MySQL {
    required init() { ... }
    func connect() { ... }
    // ...
}
class DB<DBImpl> { ... }
extension DB where DBImpl == MySQL {
    func connect() {
        MySQL().connect()
    }
}
這個方案與我們所想像的差不多,確實符合了隱式約束以及編譯期選擇
但是卻有很明顯的重複性,我們有幾個Policy就需要extension幾次,且所做的事都差不多,只能說Swift不是C++,XD

2017年6月26日 星期一

C++ thread 基礎

使用標準庫的thread非常容易
#include <thread>
#include <iostream>

using std::cout;

void hello() {
    cout << "hello" << '\n';
}

int main()
{
    std::thread t(hello);
    t.join();
}
1.引入thread標頭檔
2.宣告函式
3.建構一個thread物件
4.用join讓main等待它完成

很好,程式應該會運作,可是我們想要知道如何傳入參數,對吧!
void hello(int i) {
    cout << "hello, " << i << '\n';
}
所以函數的宣告式自然要改
但是我們不能直接寫
std::thread t(hello(2));
因為這不會傳入函數,而是傳函數的結果,那不是我們需要的東西
正確的寫法是
std::thread t(hello, 2);
可以輕鬆的從這個實作(Mingw版本)中看出參數怎麼傳進去的
template<class Function, class... Args>
explicit thread(Function&& f, Args&&... args)
{
    typedef decltype(std::bind(f, args...)) Call;
    Call* call = new Call(std::bind(f, args...));
    mHandle = (HANDLE)_beginthreadex(NULL, 0, threadfunc<Call>,
        (LPVOID)call, 0, (unsigned*)&(mThreadId.mId));
    if (mHandle == _STD_THREAD_INVALID_HANDLE)
    {
        int errnum = errno;
        delete call;
        throw std::system_error(errnum, std::generic_category());
    }
}

事實上,我們不只能傳入函數給Thread,我們可以傳任何可呼叫(callable)物件進去
用法非常簡單,就是定義一個具有operator()的class,然後用這個class產生物件
class Ya {
public:
    void operator()() const {
        cout << "Ya" << '\n';
    }
};
就像這樣
std::thread t( Ya() );
我們用原本的寫法,卻發現編譯失敗,原來是因為這個寫法被編譯器當作函式宣告,而不是一個物件定義

好吧!怎麼處理?
第一種作法:加上括號
std::thread t( (Ya()) );
第二種作法:用大括號初始運算子
std::thread t{ Ya() };
第二種作法自然必較好,因為符合新的標準(用大括號是官方推薦寫法),而且很直觀
第一種作法則讓人難以理解為什麼這樣就可以

再介紹一種作法
std::thread t3([] {
    cout << "lambda" << '\n';
});
利用lambda運算式,不過就算是用lambda我也認為應該用大括號運算子,畢竟,沒什麼道理不用擺明用來初始化的大括號(我是說,除了那個該死的auto array狀況,還有字串字面值是const char *)

那麼join呢?
thread物件一旦建立,啟動執行緒,你就要明確的決定要
1.等待執行緒結束(join)
2.讓它自己旁邊玩沙(deatch)

如果沒有在thread物件被清除之前決定,那程式就會終止
因為
~thread()
{
    if (joinable())
        std::terminate();
}
解構子會呼叫std::terminate()讓程式掛掉(如果沒有改變可連結狀態)

bool joinable() const {return mHandle != _STD_THREAD_INVALID_HANDLE;}
這是joinable的實作,因為名稱取的很好,所以可以看出只要thread狀態沒有被合法的處理(上面兩個狀況,join與detach),就會回傳true,在適當的時候引發terminate

所以即使發生例外,也要確保執行緒成功被決定要怎樣處理
從這裡應該很容易看出來,thread物件可不是thread本身,而是持有者,所以千萬不要搞混它們的意義

要讓程式掛掉真的很容易
std::thread t( hello, i );
不決定的結果就是程式panic

例外!!!
沒錯,什麼程式遇不到例外,執行緒程式也不例外,前面我們提到,如果沒有決定如何處理thread物件,程式就會掛掉
很好,那麼遇到例外時怎麼辦?
第一種辦法很土,不過反正能解決問題就是了
std::thread t(hello);
try {
    // ...
} catch (int err) {
    t.join();
}
t.join();
看,就是寫兩次而已,這真的很糟糕
因為我們很可能會忘記寫某一個join,然後沒看到,或是當下看不出來,最後trace bug還看到terminate然後想----我為什麼會呼叫terminate?恩,因為你沒有呼叫,最後憤怒的找到thread函式庫

第二種辦法是RAII
class Thread_guard {
    std::thread t;
public:
    explicit Thread_guard(std::thread& t_)
        : t{t_}
    {}
    ~Thread_guard() {
        if (t.joinable()) { t.join(); }
    }
}
現在我們把thread放進去就好了,值得一提的是,這種物件最好移除複製建構子和複製指派運算子
Thread_guard(Thread_guard const&) = delete;
Thread_guard& operator=(Thread_guard const&) = delete;
因為兩種操作對這個物件而言都異常危險,我們將無法預測會發生什麼事

宣告為delete之後,試圖做上述操作都會直接被編譯器擋下
用法非常明確
std::thread t{func}
Thread_guard tg{t}

// do something ...
這樣一來,只要離開資源,tg的解構式被啟動,就會決定怎麼處理thread物件(這仰賴c++對解構的保證)

最後,注意cout其實不能那樣用,你可以試試使用迴圈讓執行緒印更多東西,然後你會發現文字會不按順序的亂印,這是正常的,因為它們交錯的使用cout,而沒有一個資源管理的方式
最簡單的方式就是上鎖,當然也有對這類行為不太介意的程式,例如共享的資源是唯獨的

2017年5月18日 星期四

Remove Element

Leetcode easy題(嗯,真的很easy)
class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        while( haveVal(nums, val) ) {
            nums.erase(std::find(nums.begin(), nums.end(), val));
        }
        return nums.size();
    }
private:
    bool haveVal(vector<int> nums, int val) {
        for(int i=0; i<nums.size(); i++) {
            if(nums.at(i) == val) return true;
        }
        return false;
    }
};
重點在善用STL幫助你省事,畢竟C++的各個容器都並非非常完整、操作簡單的那種東西
相反的它非常鼓勵各個組件組合應用這種形式
By the way,Algorithm中的幾個方法還不如像這樣自行實現方便,因為他們被設計成適合解泛化問題的形式

ps. Python只要這樣寫就行了,選錯工具,誤你一生(誤www)
def removeElement(list, val):
    while val in list:
        list.remove(val)
    return len(list)

這則是利用了find的版本,可以看得出來有些冗長
public:
    int removeElement(vector<int>& nums, int val) {
        while(std::find(nums.begin(), nums.end(), val) != nums.end()) {
            nums.erase(std::find(nums.begin(), nums.end(), val));
        }
        return nums.size();
    }

2017年5月7日 星期日

GoogleTest安裝(on OSX)

這篇來介紹怎麼在Mac上使用google的開源測試框架----googletest
ps. 雖然是說在OSX,不過linux應該能用幾乎一樣的方式成功

首先切換到你想要放原始碼的地方
然後輸入指令
$ git clone https://github.com/google/googletest.git
沒錯,要使用上述功能需要先擁有git(一個優秀的版本控制工具)
但是你也可以選擇下載zip之類的檔案解壓縮

總之,取得原始碼之後,先進入專案目錄中(注意,這是用git下載的狀況,使用解壓縮的方式必然有所不同)
$ cd ~/googletest/ $ mkdir install $ cd install
然後建立目錄並進入

使用cmake
$ cmake -DCMAKE_CXX_COMPILER="c++" -DCMAKE_CXX_FLAGS="-std=c++11 
-stdlib=libc++" ../
有關cmake,可以使用homebrew安裝,省時省力,讓你多一點時間睡覺XD
上面加上的兩個參數,分別是 指定編譯器 和 指定C++版本,最後一個參數是建置目標,目標目錄中必須要有CMakeLists.txt這個檔案,裡面定義了建置規則供cmake運作

接著編譯檔案並安裝
$ make #編譯程式碼 $ sudo make install #安裝程式碼

這裡讓環境變數指向C++程式庫位置
$ echo "export CPLUS_INCLUDE_PATH=/usr/local/include" >> ~/.bash_profile $ echo "export LIBRARY_PATH=/usr/local/lib" >> ~/.bash_profile $ source ~/.bash_profile

再來你就自己看看怎麼使用googletest,建立專案測試是否安裝成功吧
ps. 之後發現一個很蠢的錯誤,我少了一個'/'符號,所以一直出現ld錯誤,更改.bash_profile之後就可以了