作者PkmX (阿貓)
看板C_and_CPP
標題Re: [問題] Private method 該不該確認參數正確性?
時間Tue Jan 27 22:17:31 2015
一般來說比較好的方式應該是把參數的condition包在type裡面,
以你的例子來說,sqrt的參數不能<0,那就創一個新的class把他包起來:
class non_zero_float {
public:
non_zero_float(float v_) : v(check(v_)) {}
auto operator()() const -> float { return v; }
private:
auto check(float v) -> float {
if (v < 0.0f) {
throw std::runtime_error("value < 0.0f");
} else {
return v;
}
}
float v;
};
然後你的public/private methods寫成這樣:
auto private_sqrt(const non_zero_float nzv) -> float {
return std::sqrt(nzv());
}
auto public_sqrt(const non_zero_float nzv) -> float {
return private_sqrt(nzv);
}
呼叫public_sqrt的方式就變成:
public_sqrt(non_zero_float(2.0f))
對使用者來說,要呼叫{public,private}_sqrt都必須建構出一個non_zero_float,
也就是說這個API設計會讓compiler reject錯誤的使用方式(也就是沒有檢查),
在建構時進行參數檢查,而建構完成後該值就不能再更動(immutable)了,
所以接下來使用的時候無論pass給誰,都不需要再額外進行check。
我們甚至可以把上面的作法generalize:
template<typename T, typename Predicate>
class with_predicate {
public:
with_predicate(T t_, Predicate pred = Predicate()) : t(pred(t_)) {}
auto operator()() const -> const T& { return t; }
private:
T t;
};
而上面的non_zero_float就可以寫成:
struct non_zero_predicate {
auto operator()(const float v) -> float {
if (v < 0.0f) {
throw std::runtime_error("value < 0.0f");
} else {
return v;
}
}
};
struct non_zero_float : with_predicate<float, non_zero_predicate> {
non_zero_float(const float v)
: with_predicate<float, non_zero_predicate>(v) {}
};
或是要接受一個已經sort好的std::vector<int>:
struct is_sorted_predicate {
template<typename Container>
auto operator()(const Container& c) -> const Container& {
if (std::is_sorted(std::begin(c), std::end(c))) {
return c;
} else {
throw std::runtime_error("container is not sorted");
}
}
};
struct sorted_int_vector
: with_predicate<std::vector<int>, is_sorted_predicate> {
sorted_int_vector(const std::vector<int>& v)
: with_predicate<std::vector<int>, is_sorted_predicate>(v) {}
};
當然你也可以讓Predicate不要throw exception,直接想辦法return一個正確的值,
(或許這個function object不要叫Predicate比較好...)
舉個實際的應用來說,
在寫3D math的函式時,可以把三維向量(vec3)和三維的單位向量(uvec3)分開,
而uvec3只能透過vec3做normalize後才能建構出來,
auto normalize(vec3 v) -> uvec3;
計算時如果需要input為單位向量,就宣告input的型態為uvec3,
這樣如果使用的時候不小心把不是單位向量的vec3傳過去,compiler時就會出錯
這個概念甚至可以做更多延伸,例如把點(point)和向量(vec)的概念分開,
然後僅支援合理的運算,例如:
point + vector -> point
point - point -> vector
而如果寫出例如「point + point」這種沒定義的運算就會在編譯時產生錯誤
其實有很多這種API design的技巧可以運用compiler來限制API正確的使用方式,
多想幾分鐘你可以不用呆呆的一直用float*然後還要花時間加上註解說明,
C++的class不是只是用來把data和method綁在一起讓你可以少傳一個this而已
------------------------------------------------------------------------------
題外話:
這種type + logical predicate的模式在程式語言學中稱作refinement types,
舉個例子來說,假設f1的參數要求必須是介於40和60之間的int,
我們可以把這個參數乘二後傳給另一個要求參數介於0~100之間的f2嗎?
如果用上面C++的寫法,可能必須定義出各種數值區間的型態以及他們之間的各種轉換,
數量一多programmer根本無法去做處理,所以只能應用在一些簡單的case上,
但這個例子如果在支援refinement types的LiquidHaskell底下,就會寫成:
{-@ f1 :: { n : Int | n >= 40 && n <= 60 } -> () @-}
f1 :: Int -> ()
f1 n = f2 (n + n)
{-@ f2 :: { n : Int | n >= 0 && n <= 100 } -> () @-}
f2 :: Int -> ()
f2 n = ()
編譯器會用SMT solver幫你證明f1的參數可以傳給f2,
但很明顯這個程式是有問題的(n > 50就會超出f2的範圍),所以編譯時會直接產生錯誤,
而如果將上述f2的條件改成n >= 80 && n <= 120這樣編譯時就沒有問題
--
※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 140.113.88.55
※ 文章網址: https://www.ptt.cc/bbs/C_and_CPP/M.1422368253.A.8D6.html
推 Killercat: 這個給個推 也是個非常不錯的方案 01/27 23:52
→ Killercat: 尤其C++有template 有各種奇奇怪怪的overload 01/27 23:52
推 Killercat: 不過我會建議你再增加一個explicit operator float() 01/27 23:58
→ Killercat: 這樣會更方便 可以直接把這個class當float來用 01/27 23:58
→ Killercat: 不過請務必不能漏掉explicit 不然他轉型很難控制 01/27 23:59
→ PkmX: 喔對 用explicit operator float應該會比較好 01/28 00:22
→ PkmX: 剛剛第一個想到是用operator()應該是我最近scala寫太多=.= 01/28 00:22
→ suhorng: scala xD 01/28 00:38
※ 編輯: PkmX (140.113.88.55), 01/28/2015 00:46:06
推 Caesar08: 推,學到一課 01/28 05:00
推 Ebergies: 感覺是個不錯的做法, Secure Coding in C 也有講到 01/28 10:46
→ Ebergies: Refinement types 01/28 10:46
推 BlazarArc: 關於下面的是否可定義 ValidInt(int n, irange r) 然後 01/28 11:17
→ BlazarArc: 實作 ValidInt::Multiply 之類的呢? 01/28 11:18
推 BlazarArc: 好像漏了一點,大概知道難度在哪... 01/28 11:20
推 lc85301: 高手 01/28 14:58
→ a27417332: 推一下,不過單是看code感覺就眼花了,有點好奇實務上 01/28 17:26
→ a27417332: 真的會有不少人這樣檢查嗎OAO 01/28 17:26
推 a27417332: 結果說要推卻忘記推,補推一下(囧) 01/28 17:28
推 carylorrk: 學習學習 01/29 00:05
推 Killercat: 實務上就是一個template而已 檢查條件可以用policy 01/29 11:36
→ Killercat: 達成,其實這個已經算是夠完整的實作了 01/29 11:36
推 FukadaKyoko: 強大!! 02/12 16:23