一般来说比较好的方式应该是把参数的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而已