Reputation: 6854
Is there a natural way to define a number type between 0 and 1? Essentially one could do everything with double, the problem is that I should define some bound checking in some functions and I had the (probably stupid) idea to outsource the bound checking to a class, something like
class Probability {
// the value of the Probability
double val;
Probability(double val):value(val){
// freak out if val > 1 or val < 0
//...
//
};
// operators such as
Probability operator + (Probability const & a, Probability const & b){
double result a.val + b.val;
if ((result > 1) || (result < 0)){
// freak out
result = 0
}
return result;
}
// ...
//
}
The problem with this approach is probably that it slows down every operation. Is there a faster way of bound checking? I also would like to know how to handle the "freak out" sections in the code above.
Upvotes: 0
Views: 678
Reputation: 5352
You can use the class Probability
to enforce the bounds, storing a double internally. If you want operations such as P(0.75) + P(0.5) - P(0.6)
to work, you can have the operators return a proxy object, which doesn't do the bounds checking. This proxy object would have a conversion operator to Probability
, and the Probability
constructor would check the bounds. If you only ever use the Probability
type directly, and allow temporaries of the form ProbabilityResultProxy
, you'll get the desired behaviour.
The example below outlines this approach. Obviously there's quite a lot missing that you'd want in a real implementation, but I wanted to focus on a specific solution, rather than providing a complete class.
#include <iostream>
class Probability {
public:
Probability(double value) {
if (value < 0 || value > 1) throw std::runtime_error("Invalid probability");
value_ = value;
}
double value() const { return value_; }
private:
double value_;
};
class ProbabilityResultProxy {
public:
explicit ProbabilityResultProxy(double p) : value_(p) {}
double value() const { return value_; }
operator Probability() {
return Probability(value_);
}
private:
double value_;
};
ProbabilityResultProxy operator+(const Probability& lhs, const Probability& rhs) {
return ProbabilityResultProxy(lhs.value() + rhs.value());
}
ProbabilityResultProxy operator+(const ProbabilityResultProxy& lhs, const Probability& rhs) {
return ProbabilityResultProxy(lhs.value() + rhs.value());
}
ProbabilityResultProxy operator+(const Probability& lhs, const ProbabilityResultProxy& rhs) {
return ProbabilityResultProxy(lhs.value() + rhs.value());
}
ProbabilityResultProxy operator+(const ProbabilityResultProxy& lhs, const ProbabilityResultProxy& rhs) {
return ProbabilityResultProxy(lhs.value() + rhs.value());
}
ProbabilityResultProxy operator-(const Probability& lhs, const Probability& rhs) {
return ProbabilityResultProxy(lhs.value() - rhs.value());
}
ProbabilityResultProxy operator-(const ProbabilityResultProxy& lhs, const Probability& rhs) {
return ProbabilityResultProxy(lhs.value() - rhs.value());
}
ProbabilityResultProxy operator-(const Probability& lhs, const ProbabilityResultProxy& rhs) {
return ProbabilityResultProxy(lhs.value() - rhs.value());
}
ProbabilityResultProxy operator-(const ProbabilityResultProxy& lhs, const ProbabilityResultProxy& rhs) {
return ProbabilityResultProxy(lhs.value() - rhs.value());
}
int main() {
Probability p1(0.75);
Probability p2(0.5);
Probability p3(0.6);
Probability result = p1 + p2 - p3;
std::cout << result.value() << "\n";
try {
Probability result2 = p1 + p2;
std::cout << result2.value();
} catch (const std::runtime_error& e) {
std::cout << e.what() << "\n";
}
return 0;
}
Here, the mathematical operators are defined for each combination of Probability
and ProbabilityResultProxy
. Each operation returns a proxy object, and the final assignment causes the bounds check to be performed.
If you want, you could make the ProbabilityResultProxy
a private member class of Probability
, and make the operators friends of Probability
. This prevents anyone from instantiating the proxy class directly.
Upvotes: 2
Reputation: 179991
The "freak out" option is simple: throw std::invalid_argument
. It doesn't matter that throwing is a bit slow; you're not getting an answer anyway. The upside is that not throwing is fast as the optimizer can assume the non-exception paths are far more likely.
Performance-wise it might be beneficial to have a IntermediateResult
class such that only the final assignment back to Probability
is range-checked. This sidesteps the 0.75+0.5-0.6
example.
Upvotes: 1