一般來說比較好的方式應該是把參數的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而已