常見的錯誤 : return str.c_str()

這個錯誤好像通常不太會炸... 不過由於是未定義行為,所以炸不炸鍋會看運氣。

最經典的例子是繼承std::exception

class myexception : std::exception {
    myexception(...) : std::exception() {}
    const char *what() const noexcept override {
        return .....; 
    }
};

老實講我不知道為什麼exception當初要這樣設計,舊有的C並沒有exception的概念,而既然是從C++才開始引入,卻不是用string or stream卻使用超舊世代問題滿滿的const char*來當回傳值,這滿奇怪的。不過讓我們看看各種嘗試 :

丟出literial string (C-style String)

基本上這兩者編譯起來,是完全一樣的 :

const char* myexception::what() const noexcept {
    return "Hello World!";
}

//or

const char* myexception::what() const noexcept {
    const char* ret = "Hello World!";
    return ret;
}

先看看第二個,看起來很像傳回了一個Dangling Pointer,但是事實上ret的位置在絕大多數的compiler都會指向.rodata或者.data,所以雖然沒有保證,但是這個指標生命週期並不是僅止於what()

第一個其實跟第二個完全一樣,只是ret變成匿名物件而已。

同樣是const char*, 看看std::string的場合?

通常來講exception會需要動態產生what(),在log裡面通常就是印出what()。所以動態產生what()內容的話,首選當然是std::string。這範例寫法頗爛,不過不影響,正常產生格式化字串我會推boost::format()

const char* myexception::what() const noexcept {
    std::string ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

這種寫法是最多人犯的錯誤(但是奇怪的是,爆炸率似乎不高?),同樣是const char*,這樣寫是有問題的。std::string為了相容性的原因提供了c_str(),主要是為了讓以前能吃c-style string的東西能吃得進去,為了避免外界沒透過std::string就改內容造成錯誤,所以回傳const char*

但是ret本身就是一個local variable, 一般來說回傳ret有沒有問題通常是看函數簽名回傳的是value還是reference,後者的話穩死不用說,大家都是專家,前者的話則會啟動copy constructor。先不討論回傳值最佳化Copy Elision,又稱為RVO或者std::move

//沒事,會啟動copy constructor(不討論ROV)
std::string test() {
    std::string ret = {"Hello World!"};
    return ret;
}

//有事,回傳一個local variable的reference
std::string& test() {
    std::string ret = {"Hello World!"};
    return ret;
}

既然大家都知道local變數的生命週期僅止於function scope,那這個local的內部指標當然生命週期也僅止於這個local。回到上一個例子

const char* myexception::what() const noexcept {
    std::string ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

附帶一提,有想過為什麼std::string不提供隱式轉換成const char*嗎?這是其中一個原因。不過大家都知道,他有提供const char*隱式轉換成std::string的轉換operator。

那改用static?

const char* myexception::what() const noexcept {
    static std::string ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

可以,但是代價很大。這會造成thread unsafe以外,所有的exception都共享一個ret。當你需要exception包exception的時候,或者multi-thread的時候,這會造成一些讓人抱頭痛哭的問題。

所以....正確答案是類別變數?

const char* myexception::what() const noexcept {
    this->m_ret = std::string("Error message is : ") + m_err;
    return ret.c_str();
}

很不幸這東西無法通過編譯,std::exception的what()被後綴const保護著,而繼承者無法鬆掉這層限制。後綴const基本上會阻止你的類別變數被修改跟讀寫,而且會造成一些滿難懂的錯誤訊息。

所以我想要客製化what()的回傳該怎麼辦?

答案是,其實what()呼叫時機已經是很後面了,所有該有的資訊都有了,所以你不該在裡面做non const的行為。你應該在exception建構子裡面就把這件事情做好,拿我的專案為例子:

namespace Iris {
    class SqlException : public DPLException {
        std::string sql;
    public:
        SqlException(const std::string &reason, const std::string &sql_command = "",
                     const std::string &driver_output = "")  noexcept;
        const char *what() const noexcept override;
        ~SqlException() override = default;
    };
}


const char *Iris::SqlException::what() const noexcept {
    return what_output.c_str();
}


Iris::SqlException::SqlException(const std::string &reason, const std::string &sql_command,
                                 const std::string &driver_output) noexcept : DPLException(reason, driver_output) {
    this->sql = sql_command;
    boost::format output =
            boost::format(
                    "\nSQL Exception raised\nOS message : %1%\nsql_command : %2%\nreason : %3%\ndriver_output : %4%\n")
            % exception::what() //Don't use what(), it will cause loop. Dont use DPLException::what() too, will include duplicated message
            % sql
            % reason
            % driver_output;
    what_output = output.str();
}

通常來講,一開始就這樣做會少走一點冤枉路。

在CMake裡面使用Google Test

之前想在CMake專案裡面使用,似乎很少有人提到如何把Google Test跟CMake很好的整合起來,所以寫了一篇簡單的心得來做個紀錄。

首先取得Google Test

我個人的習慣是把目錄分成這幾個目錄,我拿我現在正在寫的一個小專案來做例子:

.
├── CMakeLists.txt
├── README.md
├── deps
│   └── googletest
├── main.cpp
├── src
│   └── IHost.h
└── test
    └── twitter_basic.cpp

其中deps專門用來放3rd party dependency。我個人是不太建議使用系統安裝來裝這種那麼小的東西,而且這會造成別人使用這個project的麻煩。

我自己是習慣使用git submodule來裝這種git專案就能拿到的東西,所以取得方法會像是這樣 :

➜  Octo git:(master)cd dep
➜  dep git:(master) ✗ git submodule add git@github.com:google/googletest.git
Cloning into '/Users/rayer/Develop/Octo/dep/googletest'...
remote: Counting objects: 7670, done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 7670 (delta 3), reused 7 (delta 2), pack-reused 7654
Receiving objects: 100% (7670/7670), 2.61 MiB | 356.00 KiB/s, done.
Resolving deltas: 100% (5686/5686), done.

這樣deps底下就會有googletest這個子目錄了。其他人clone你的專案的時候,記得在README.md提醒一下對方要先git submodule init來取得所有的submodule。

當然取得google test的source code方法不只一種,你可以直接在deps裡面下git clone,或者直接download source code硬解到底下,結果應該差不多。

CMake裡面的設定

Google Test完整支援CMake,所以整個構造會很簡單。其中下面的test/twitter_basic.cpp就是你寫你TEST TEST_F TEST_P的位置,請自己建立一個檔案。

# 可加可不加,在這個範例不影響
enable_testing()

# 把google test整個cmake managed dir全部加進來
add_subdirectory(deps/googletest)

# 宣告include目錄有google test,通常順便連googlemock一起加入
include_directories(deps/googletest/googletest/include)
include_directories(deps/googletest/googlemock/include)

# 測試的主體。main宣告google test已經幫你宣告好了,直接用就是,就是gtest_main.cc
add_executable(twitter_test deps/googletest/googletest/src/gtest_main.cc test/twitter_basic.cpp)
# Link gtest library
target_link_libraries(twitter_test gtest)

這樣基本上就能跑了,看起來會像這樣:

Running main() from gtest_main.cc
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from SANITY
[ RUN      ] SANITY.sanity
[       OK ] SANITY.sanity (0 ms)
[----------] 1 test from SANITY (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (0 ms total)
[  PASSED  ] 1 test.

我在寫Daemonizer(註一)的時候,碰到了一個很奇怪的語法錯誤

ifstream f(this->pid_file_name());
string content(istreambuf_iterator<char>(f), istreambuf_iterator<char>());
pid_t pid = stoi(content);

他會跑出error,聲稱content是一個function所以無法當做stoi的引數。然而,要是這種傳法就能work

string content("123");
// OR
string content = string(istreambuf_iterator<char>(f), istreambuf_iterator<char>());

這錯誤的主要成因是,第一個例子中的string content(istreambuf_iterator<char>(f), istreambuf_iterator<char>());被錯誤的當成了function prototype(話說,哪個use case會要在函數本體內宣告另外一個函數的prototype?)。這種情況叫做Most vexing parse

C++11提供了一個統一初始化方法,也就是說,你用一個大括號把它包起來,他就一定會被認為是個物件初始化,而不是一個function prototype。C++11以前的話,只能再多一組括號把它包起來。

//C++11
string content{istreambuf_iterator<char>(f), istreambuf_iterator<char>()};
//C++11以前的方法
string content( (istreambuf_iterator<char>(f), istreambuf_iterator<char>()) );

真是個討人厭的慣例啊(抓頭)....

(註一) 一個類似daemon的linux application,但是他是code層級,你只要繼承他,他就會幫你搞定所有Daemonize的事情

最近在玩AngularJS跟Google Application Engine,
PyCharm預設的組合就是Flask + Jinja2
配著Flask教學加上AngularJS教學,三分鐘就踢到了鐵板

<div ng-app="">
        <p>Name : <input type="text" ng-model="name"></p>
        <h1>Hello {{name}}</h1>
</div>

這個簡單到不能再簡單的東西,在網頁上永遠是錯誤的結果
本來對話筐輸入會馬上呈現在Hello後面,但是始終Hello後面都是一片空白
(正確結果請參照 這裡 )

本著實事求是的精神,打開safari收到的html仔細端詳了一下,原來他變成

<div ng-app="">
        <p>Name : <input type="text" ng-model="name"></p>
        <h1>Hello </h1>
</div>

搞個半天原來是{{ }}同時也是Jinja2的data binder的識別符號
Jinja2在render這個html的時候(render_template() )順手把沒bind到的東西移除了

當然直接拿掉Jinja2的renderer也是個方法
(就把每個endpoint最後的return render_template('template.html')
改成直接輸出template.html內容就可以了)
不過這種CGI式的renderer還是在某些場合挺方便的,不想這樣說宰就宰

後來查個半天,把jinja2的data binder識別符號換掉就好
不過網路上找到的解法是建議順便把其他兩組一起替換掉

jinja_options = app.jinja_options.copy()

jinja_options.update(dict(
        block_start_string='<%',
        block_end_string='%>',
        variable_start_string='%%',
        variable_end_string='%%',
        comment_start_string='<#',
        comment_end_string='#>'
))
app.jinja_options = jinja_options

暫時解決了這個問題了... 三分鐘就撞牆,花30分找解法 =口=

Markdown一個最麻煩的地方就是缺乏文字色彩的原生支援。之前查了網頁,10個裡面有8個說沒有,兩個說用css。但是後來我想起來Markdown本身就有原生支援html語法,那就簡單了...

我們要把<font color="red">文字</font><font color="blue">變成</font><font color="green">彩色</font>

我們要把文字變成彩色

恩,其實滿直覺的,怎麼一開始沒想到這招囧...

在幾天以前,SourceForge啟動了一項頗具爭議性的政策 : 他們擅自「接管」了一些他們認為「被遺棄」的,本來在SourceForge host的軟體,重新包裝了installer並且植入了第三方軟體,比方說惡名昭彰的MacKeeper等等。

在GitHub崛起以前,SourceForge一直是大多數開源軟體首選的host地點,就好像我們的OpenFoundary一樣。然而在GitHub等等免費託管服務越來越盛行的同時,SourceForge也有越來越多的使用者改用GitHub,而SourceForge上面的軟體就比較沒有在更新了。

在幾天前,SourceForge擅自對「被認定遺棄」的軟體做了一些調整,他們重新包裝了這些專案的installer,暗地在裡面放入了臃腫或者廣告軟體。最早發現這事情的專案擁有者是Gimp for Windows,這引發了開源社群的全面...暴怒(該用這個形容詞嗎),比分說NotePad++作者就憤怒地把專案完全從SourceFor遷出去。

Obviously, the paid component per installation system is one of their important income generating scams. I would be fine with that, if they were the actual owners of the legitimate software. The real problem is, they are polluting these open source software installations for the purpose of filling their pockets by this scam, and worst of all, without even notifying the authors/creators of this software, while the creators are struggling against such parasitic software in order to keep their installers cleaner and safer.

顯然這些付費嵌入的程式,是他們一個很重要的收入 - 以及詐騙的來源。老實講,如果嵌入者是這些專案的擁有者,我還不會那麼在意。真正的問題是,他們正在污染整個open source的軟體,靠這些根本是詐欺的手段去填滿他們的錢包,而且更糟糕的是,甚至連致力於對抗以及清除問題軟體,讓專案installer更安全的專案擁有者都不通知一聲。

Such a shameless policy should be condemned, and the Notepad++ project will move entirely out of SourceForge.

這種無恥的行徑應該要被強烈譴責,我宣布Notepad++將完全移出SourceForge。

其實SourceForge做這種事也不是第一天了,以前他們會在某些Windows Installer在有知會開發者的前提下綑綁軟體。不過,明目張膽的「接管」他們認定「被遺棄」的軟體來玩這套,倒是真正的引發了眾怒。

SourceForge知道了這事情的嚴重性,在6/3宣告停止這種行為(除非開發者允許)。不過看起來這次SourceForge對開發者來講,損失的信心恐怕很難再回來了。

延伸閱讀:他連NMAP都敢動耶!

這篇是我兩三年前寫在Levitation的blogspot,由於排版很爛,所以回收到這裏重新用Markdown寫一次

淺談OAuth in Android -- 以Plurk為例子

OAuth是目前比較熱門的認證機制之一,他有個最大的好處就在於完全不需要在client端輸入賬號密碼,這部分完全會由web端完成。不過,他機制非常的複雜,有些地方做的讓人覺得很龜毛,所以我們在這篇只要淺談就好,其他的部分我會介紹一個函式庫來把它完成。

這邊有一篇非常不錯的OAuth機制介紹,有興趣的朋友可以看看。太細節部分的工作原理我們就不涉足了,我們專心看看最基本的認證步驟

  1. app開發者跟網站註冊取得API Key跟API Secret
  2. 開始認證的時候,用這組key/secret/callback(1)跟網站要一個Request Token(2)
  3. 用這組Request Token, callback組合成一個網址,讓使用者進入該網址認證。
  4. 認證成功以後會傳回一個oauth_verifier,通常是一個很好閱讀的字串或者數字
  5. app端把這組verifier,以及request key跟網站取得半永久性的Access Token(*2)
  6. 往後對這個網站任何要求(比方說,plurk來講,取得時間軸)使用這個Token即可,app把它存起來備用,就不需要重新跟網站申請認證了。

這幾個步驟其實相當的繁瑣,尤其是對網站要求每次都要做一個全面性的Parameter Sign的動作,裡面牽涉的東西對一個初學者來講真是煩人到爆炸。當然,就會有人會寫一組library來解決這問題:我們今天介紹的就是oauth-signpost library。

基本的取得jar,放進project,設定buildpath這些基礎到爆的東西我們就不討論了。首先,我們要先去Plurk申請一組API Key來作為我們的開發用途。所以我們就有了API Key/Secret了。接下來,回到Plurk API介紹的頁面,他提供了幾個網址供OAuth使用。基本上這些分別是幹嘛的我們就不予深究了,我們只要知道她是要怎麼用。

signpost我們會用到根OAuth有關的一共有兩個部分:OAuthConsumer代表的是我們APP端的所有資料,包含Access Token等等(不過她好像沒幫你存,你要自己存),而OAuthProvider則是代表網站的OAuth認證部分。

 static final String PLURK_REQUEST_URL = "http://www.plurk.com/OAuth/request_token";
 static final String PLURK_AUTHORIZATION_URL = "http://www.plurk.com/m/authorize";
 static final String PLURK_ACCESS_URL = "http://www.plurk.com/OAuth/access_token";
 static final String PLURK_CALLBACK_URL = "myplurk:///";
 static final String PLURK_CONSUMER_KEY = "你的API Key";
 static final String PLURK_CONSUMER_SECRET = "你的API Secret";

我們先把這些東西設定變數,等等會用到。接下來我們設定Provider跟Consumer

 mainConsumer = new DefaultOAuthConsumer(PLURK_CONSUMER_KEY, PLURK_CONSUMER_SECRET);
 mainProvider = new DefaultOAuthProvider(PLURK_REQUEST_URL, PLURK_ACCESS_URL, PLURK_AUTHORIZATION_URL);

ok,那所有東西差不多就完成了。我們要讓使用者認證的時候,只要開一個url,載入provider提供的url :

 String url = mainProvider.retrieveRequestToken(mainConsumer, PLURK_CALLBACK_URL);

利用webview打開這個以後,把webview設定一個WebViewClient,然後override它的link click

private class AuthClient extends WebViewClient {
 
 
  @Override
 public void onPageStarted(WebView view, String url, Bitmap favicon) {
  super.onPageStarted(view, url, favicon);
 }
  @Override
 public void onPageFinished(WebView view, String url) {
  super.onPageFinished(view, url);
 }
  @Override
 public boolean shouldOverrideUrlLoading(WebView view, String urlString) {
 
 Log.d("SubPlurkV2", "url : " + urlString);
 if(urlString.contains("subplurkv2")) {
 Uri url = Uri.parse(urlString);
 String verifier = url.getQueryParameter("oauth_verifier");
 try {
 SystemManager.getInst().getAuthManager().aquireAccessToken(verifier);
 } catch (Exception e) {
 // TODO Auto-generated catch block
 e.printStackTrace();
 }
 
 finish();
 }
  return super.shouldOverrideUrlLoading(view, urlString);
 }

然後我們可以取得verifier,利用這個取得Access Token

 mainProvider.retrieveAccessToken(mainConsumer, verifier);
 Log.d("SubPlurkV2", "Token = " + mainConsumer.getToken() + " and secret = " + mainConsumer.getTokenSecret());

把這祖Token記錄起來,以後會用到。最後,我們來試試看最簡單地拿取時間軸吧

mainConsumer.setAccessToken("U5GNYyH6wVGS", "aCtJ9XVgNTeIpigq3qxsLi70Sv0HbA6h");
 URL url = new URL("http://www.plurk.com/APP/Timeline/getPlurks");
 HttpURLConnection request = (HttpURLConnection) url.openConnection();
 request.setDoOutput(true);
 request.setRequestMethod("POST");
 mainConsumer.sign(request);
 request.connect();
 String context = StreamUtil.InputStreamToString(request.getInputStream());
 Log.d("SubPlurkV2", "" + context);

have fun! 參考plunk api list繼續實做其他的api吧!

花了一點時間搞定了Redmine,眉眉角角還挺多的。

首先我省麻煩,直接從Digital Ocean租一個最便宜的5USD/mo的instance應該是夠的(Digital Ocean的Redmine image似乎有最佳化過,我在Linode的1G instance都build不起來,但是在DO不用Build,直接在0.5G可以裝起來的)

選他的Redmine image,其實熟手的話直接用Docker裝也是可以的。

接下來大家都很熟,DO會寄信給你告訴你Root帳號密碼。Login以後還會多一行字

-------------------------------------------------------------------------------------
These instructions are stored in /root/REDMINE for your review.
-------------------------------------------------------------------------------------
To finish installing Redmine, navigate to your droplet's IP: http://123.456.22.11 and use credentials from below.
-------------------------------------------------------------------------------------
User: admin
Pass: CIdAcEEkcC(當然這是改過的)
-------------------------------------------------------------------------------------
Make sure to specify hostname from DO panel to your droplet before creating it (for example: 'project.mydomain.com' or 'myproject.com')
This will create the necessary Nginx configs based on hostname and Nginx will respond based on hostname.
-------------------------------------------------------------------------------------

基本上已Redmine來講這樣就好了。但是如果我們自己有DNS用CNAME連過去的話會發現,他永遠處於「沒裝好」的狀態。這時候就得改/etc/nginx/sites-enabled/redmine

server {
        listen 80;
        server_name 104.236.144.91 redmine.rayer.idv.tw;

        root /srv/redmine/public;
        passenger_enabled on;

        client_max_body_size 10m;
}

server_name要放入正確的CNAME,這樣redmine才能夠正確的讀取到。

接下來就是安裝git。先在本機端把git裝起來

sudo apt-get install git

然後要去redmine的設定檔裡面更新git路徑。我們可以從上面的redmine nginx設定檔看到,他root在/srv/redmine/public,不過我們要去他的設定目錄/srv/redmine/config/改他的configuration.yml。不過,沒這檔案...請cp configuration.yml.example configuration.yml,幫他把git路徑補起來

scm_git_command:   /usr/bin/git

最後重開nginx,收工。

sudo service nginx restart

基本上用Docker就更簡單了,我本來還在想要不要包一個docker image來用,不過網路上已經有人包好了 (https://github.com/sameersbn/docker-redmine )

今天看到有人在討論版上討論,關於一個雙層for迴圈該怎麼最佳化的問題。我每次看到這個問題都會想到這個往事....

「誒某R,我把某段Code最佳化了,我把時間複雜度從O(n^2)降成O(n)」
「喔喔,這還真是滿大的突破,那段還滿吃緊的呢。你是怎麼做到的?」
「簡單啊,看我的code」

before :

for(int i = 0; i < m; ++i)
    for(int j = 0; j < n; ++i) {
    process(target[i][j]);
    }

after :

for(int i = 0; i < m * n; ++i) {
    process(*((target*)(&target) + i));
  }

我整個下午都在一直在想,我應該要尻他腦袋尻多少下才能讓他醒過來,然後下班時間就到了。

我把這個「最佳化」寫成一個範例給大家參考一下

#include <iostream>

using namespace std;

void process(int i) {
    cout << "value = " << i << endl;
}



using namespace std;
int main(int argc, char *argv[]) {
    
    const int row = 5;
    const int col = 6;
    int var[row][col];
    
    int value = 0;

    //for initialization propose, not part of demo
 for(int i = 0 ; i < row; ++i)
        for(int j = 0; j < col; ++j)
            var[i][j] = value++;
            
    //這就是他說的before
 for(int i = 0 ; i < row; ++i)
        for(int j = 0; j < col; ++j)
            process(var[i][j]);
      
    //這就是他說的 效能增進一個數量級的after
 for(int i = 0; i < row * col; ++i) {
        process(*((int*)(&var) + i));
    }
    
}

我說的 我想的
這code這樣做很髒 這段code髒透了,不過不是我寫的
這code只是暫時性先這樣做 我寫的這段code髒透了
這段code壞掉了 你寫的這段code有bug
這段code還有一點點小問題 我寫的這段code有bug
這段code很晦澀難懂 有人寫這段code居然連個註解都不加
這段code自己就夠淺顯易懂 我寫這段code沒有註解
這是個很棒的語言 這是我最喜歡的語言,而且他用起來很方便
你要調整你對這語言的認知 這是我最喜歡的語言,但是他很難用
我看得懂這段Perl Code 這段Perl Code是我寫的
我看不懂這段Perl Code 這段Perl Code不是我寫的
這code結構很糟 別人寫了一個很糟的結構
這code結構複雜了點 我寫了一個很糟的結構
Bug 這程式少了我想要的功能
這是不在計劃內的功能 這程式少了我不想要的功能
漂亮的解法 這做法OK,而且我看得懂
我們需要重寫這一段 這做法OK,但是我看不懂
emacs比vi優秀 這裡太和平了,我們找點架來吵吧!
vi比emacs優秀 這邊太和平了,我們找點架來吵吧!
「恩...恕我直言...」 我想很婉轉地說,你他媽的錯了
「舊code」 這段code能運作,但沒人知道為什麼
^X^Cquit^[ESC][ESC}^C 救命,我不知道怎麼退出vi!