0%

About pointers on Modern C++

雖然C++11之後沒對Pointer做任何加強,不過也沒縮減他的能力
Modern C++ 不鼓勵直接使用Raw Pointer,用了一堆Toolkit做取代方案
來分析一下Raw Pointer有哪些問題以及怎麼做比較好

The problem of raw pointer

Raw Pointer最大的問題是語義不夠強
拿以下兩個例子來說

1
2
int* produce();
void consume(int *);

dosomething傳回的指標需要釋放嘛?這問題除了查看文件或是看Sourece Code外別無他法。因此很容易誤用
同樣的問題,consume參數的Pointer需要在函數中釋放嘛?假設consume釋放了記憶體,不過caller傳進來的的參數不是透過allocated拿到的(stack array or something),然後城市就掛掉了
從這兩個範例來看,你不能從函數宣告知道指標該怎麼處理
其他的Memory Leak等問題就不詳述了,以下是我對Raw Pointer和Modern C++的一些見解

Reference

Reference不是什麼新東西,C++98就有了,不過這也是有效減少Pointer issue的方式之一
Reference和Pointer的差異就不詳述了,上面兩個範例可以用Reference表示

1
2
int produce();
void consume(int&);

這樣一看,對於原先的版本,關於記憶體該誰釋放這點就很清楚了

std::vector

萬一原先的函數是要回傳一個array,而非單一元素,同樣在函數宣告無法很好的表達出來
不過用vector就知道我需要回傳一個vector

1
2
std::vector<int> produce();
void consume(std::vector<int>&);

std::string_view

假設我們要處理的是一個char array

1
2
char* produce();
void consume(char *);

如果用std::string可以更好的表達語義

1
2
std::string produce();
void consume(std::string &);

由於頻繁的Memory allcation/deallcation會造成不小的開銷
假設你餵給consume的參數是一個const char pointer,會做以下的事情

  1. 隱性的建構一個std::string物件
  2. 呼叫std::string(const char*)建構式
  3. 執行consume
  4. 結束之後把物件釋放掉
    類似的問題也在produce出現,有時在produce的回傳值我們並不需要另一個物件
    在C++98之前我們通常都這麼做
    1
    std::string& produce();
    不過如果caller方沒寫好一點用都沒有
    1
    2
    std::string& obj = produce(); // (O)
    std::string obj = produce(); // (X)
    後者還是會建立個物件, 然後呼叫Copy Constructor。
    因此C++17之後將string_view列入STL,類似的實作已經出現在各大Library了
    1
    2
    std::string_view produce();
    void consume(std::string_view);
    這樣誤用的機會又更小了

    Smart Pointers

    相信都寫過類似這樣的程式碼
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void doSomething() {
    int *arr = new int[100];
    if (cond1) {
    delete [] ar;
    return;
    }
    // Do something
    if (cond2) {
    }
    // Do anotherthing
    return;
    }
    每次都需要在每個回傳路徑檢查是否記憶體正確釋放,當重夠很多次之後,整個程式碼被遺忘的機會更多,這個時候讓編譯器幫忙可以少很多事端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void doSomething() {
    std::unique_ptr<int> arr(new int[100]);
    if (cond1) {
    return;
    }
    // Do something
    if (cond2) {
    }
    // Do anotherthing
    return;
    }
    Smart Pointer還有shared pointer和weak pointer,這裡就不細說了

std::optional

這是另外一個跟指標有關, 不過跟上面不太相同的問題
假設我們現在有個需求

  1. 搜尋一個陣列
  2. 如果找到符合條件的話, 回傳給Caller
  3. Caller使用這個符合條件的值做修改
    類似的程式碼可能長這樣
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int* findArray(int *arr, int size, int v)
    {
    for (int i = 0; i < size; i++)
    if (arr[i] == v) return arr + i;
    return NULL;
    }
    int *p = findArray(arr, size, v);
    if (p)
    *p = anotherValue;
    這個問題不能用Refernce解決,因為Reference不允許Dereference null,因此在之前的作法還是得退化至Pointer Solution
    不過有了std::optional之後,語義有所提昇
    上面的例子可以寫成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    std::optional<int&> findArray(std::vector<int> &arr, int v)
    {
    for (size_t i = 0; i < arr.size(); i++)
    if (arr[i] == v) return arr[i];
    return {};
    }
    std::option<int&> p = findArray(arr, v);
    if (p)
    *p = anotherValue;

Conclusion

雖然Raw Pointer威力強大,無所不能,但未了減少失控。做些房物措施無可厚非
用些Modern C++的技巧可以少犯不少錯誤,如果真的需要最佳化的時候,在把這些拿掉蛻化成Raw Pointer也不遲
先講究不傷身體,在講究效果..