Reputation: 1201
Suppose I have a calculator class that implements the Strategy Pattern using std::function
objects as follows (see Scott Meyers, Effective C++: 55 Specific Ways to Improve Your Programs and Designs):
class Calculator
{
public:
...
std::vector<double> Calculate(double x, double y)
{
std::vector<double> res;
for(const Function& f : functions)
res.push_back(f(x,y));
return res;
}
private:
std::vector<Function> functions;
};
where
typedef std::function<double(double,double)> Function;
Here is the problem I am facing: suppose functions f
and g
, both of type Function
, perform expensive and identical calculations internally to get the final result. In order to improve efficiency, one could wrap all the common data in a struct
, compute it once and provide to them as an argument. However, this design has several flaws. For example, this would cause a change in the signature of Function
, which can result in unnecessary arguments being passed to some function implementations. Moreover, these common and internal data are no longer hidden from other components in the code, which can harm code simplicity.
I would like to discuss the following optimization strategy: implement a class CacheFG
that:
Update
method that calculates its internal data with a given pair of doubles x
and y
; andCheck
method to determine if its current internal data was calculated with a given pair of doubles x
and y
.What one could do then is to make f
and g
to share a common instance of the class CacheFG
, which could be done using the std::shared_ptr
construct. So, below would be the creation of f
and g
functions using auxiliary functions f_aux
and g_aux
.
double f_aux(double x, double y, const std::shared_ptr<CacheFG>& cache)
{
if(not cache->Check(x,y))
cache->Update(x,y);
...
}
std::shared_ptr<CacheFG> cache;
Function f = std::bind(f_aux, _1, _2, cache);
Function g = std::bind(g_aux, _1, _2, cache);
My questions are: (1) is this a safe approach for optimization? (2) is there a better approach for solving this problem?
Edit: After a few answers, I found out that my intention here is to implement a memoization technique in C++. I remark that only the last calculated state is enough for my purposes.
Thanks to DeadMG, I will now write here just an improvement over his approach. His idea consists of using a memoization technique with variadic templates. I just offer a slight modification, where I use the construct std::decay<Args>::type
to ensure the definition of a tuple
with non-reference types only. Otherwise, functions with const-reference arguments would cause compilation errors.
template<typename Ret, typename... Args>
std::function<Ret(Args...)> MemoizeLast(std::function<Ret(Args...)> f)
{
std::tuple<typename std::decay<Args>::type...> cache;
Ret result = Ret();
return [=](Args... args) mutable -> Ret
{
if(std::tie(args...) == cache)
return Ret(result);
cache = std::make_tuple(args...);
return result = f(args...);
};
}
In order to prevent the move of result
, a copy of it is returned (return Ret(result)
) when the provided args
is the one cached.
Upvotes: 3
Views: 294
Reputation: 146930
Why create your own class? There's no need for you to fail to re-create the interface of unordered_map
. This functionality can be added as a re-usable algorithm based on std::function
and std::unordered_map
. It's been a while since I worked with variadic templates, but I hope you get the idea.
template<typename Ret, typename... Args>
std::function<Ret(Args...)> memoize(std::function<Ret(Args...)> t) {
std::unordered_map<std::tuple<Args...>, Ret> cache;
return [=](Args... a) mutable -> Ret {
if (cache.find(std::make_tuple(a...)) != cache.end())
return cache[std::make_tuple(a...)];
else
return cache[std::make_tuple(a...)] = t(a...);
};
}
I don't recall, offhand, whether std::hash
natively supports tuples. If not, you might need to add it, or use std::map
which does natively support them.
Edit: Hmm, I didn't notice that you wanted to share the cache. Well, this shouldn't be too difficult a problem, just stick an unordered_map
member in Calculator and pass it in by reference, but the semantics of doing so seem a bit... odd.
Edit again: Just the most recent value? Even simpler.
template<typename Ret, typename... Args>
std::function<Ret(Args...)> memoize_last(std::function<Ret(Args...)> t) {
std::tuple<Args...> cache;
Ret result;
return [=](Args... a) mutable -> Ret {
if (std::tie(a...) == cache)
return result;
cache = std::make_tuple(a...);
return result = t(a...);
};
}
If you want to share between several Functions, then the alteration is the same- just declare it in the class and pass in as reference.
Upvotes: 6
Reputation: 5546
Before optimizing - measure. Then if you really perform many calculations with same value - then create this cache object. I'd like to hide cache checking and updating in CacheFG::get(x, y)
and use it like const auto value = cache->get(x,y)
.
Upvotes: 2