Reputation: 137
I am trying to write a functor memoizer to save time on repeated expensive function calls. In my class design I am struggling to find a simple interface.
Using this Functor base class:
template <typename TOut, typename TIn>
class Functor {
public:
virtual
~Functor() {
}
virtual
TOut operator()(TIn input) = 0;
};
I now want to write a class that will encapsulate and memoize a functor. In addition to encapsulating a Functor
, the MemoizedFunctor
will itself be a Functor
. This results in having 3 template parameters.
Here is a working example:
#include <unordered_map>
template <typename F, typename TOut, typename TIn>
class MemoizedFunctor : public Functor<TOut, TIn> {
public:
MemoizedFunctor(F f) : f_(f) {
}
virtual
~MemoizedFunctor() {
}
virtual
TOut operator()(TIn input) override {
if (cache_.count(input)) {
return cache_.at(input);
} else {
TOut output = f_(input);
cache_.insert({input, output});
return output;
}
}
private:
F f_;
std::unordered_map<TIn, TOut> cache_;
};
class YEqualsX : public Functor<double, double> {
public:
virtual
~YEqualsX() {
}
double operator()(double x) override {
return x;
}
};
int main() {
MemoizedFunctor<YEqualsX, double, double> f((YEqualsX())); // MVP
f(0); // First call
f(0); // Cached call
return 0;
}
I feel like there MUST be a way to eliminate having to specify all 3 template parameters. Given the function passed to the constructor of MemoizedFunctor
, I would argue that all three template parameters could be deduced.
I am not sure how to rewrite the class so that using it would not require all the template specification.
I tried using a smart pointer to a Functor as the member variable in MemoizedFunctor
. This eliminates the first template parameter, but now the user of the class must pass a smart pointer to the MemoizedFunctor
class.
In summary, I would like to have all the template arguments of MemoizedFunctor
be automatically be deduced on construction. I believe this is possible because on construction all template arguments are unambiguous.
Upvotes: 3
Views: 219
Reputation: 66210
In summary, I would like to have all the template arguments of MemoizedFunctor be automatically be deduced on construction. I believe this is possible because on construction all template arguments are unambiguous.
If I understand correctly, the first template type for MemoizedFunctor
is ever a Functor<TOut, TIn>
, or something that inherit from some Functior<TOut, TIn>
, where TOut
and TIn
are second and third template parameter for MemoizedFunctor
.
It seems to me that you're looking for a deduction guide.
To deduce the second and third template parameter, I propose to declare (no definition is required because are used only inside decltype()
) the following couple of functions
template <typename TOut, typename TIn>
constexpr TIn getIn (Functor<TOut, TIn> const &);
template <typename TOut, typename TIn>
constexpr TOut getOut (Functor<TOut, TIn> const &);
Now, using decltype()
and std::declval()
, the user defined deduction guide simply become
template <typename F>
MemoizedFunctor(F)
-> MemoizedFunctor<F,
decltype(getOut(std::declval<F>())),
decltype(getIn(std::declval<F>()))>;
The following is a full compiling example
#include <unordered_map>
template <typename TOut, typename Tin>
class Functor
{
public:
virtual ~Functor ()
{ }
virtual TOut operator() (Tin input) = 0;
};
template <typename TOut, typename TIn>
constexpr TIn getIn (Functor<TOut, TIn> const &);
template <typename TOut, typename TIn>
constexpr TOut getOut (Functor<TOut, TIn> const &);
template <typename F, typename TOut, typename TIn>
class MemoizedFunctor : public Functor<TOut, TIn>
{
public:
MemoizedFunctor(F f) : f_{f}
{ }
virtual ~MemoizedFunctor ()
{ }
virtual TOut operator() (TIn input) override
{
if ( cache_.count(input) )
return cache_.at(input);
else
{
TOut output = f_(input);
cache_.insert({input, output});
return output;
}
}
private:
F f_;
std::unordered_map<TIn, TOut> cache_;
};
class YEqualsX : public Functor<double, double>
{
public:
virtual ~YEqualsX ()
{ }
double operator() (double x) override
{ return x; }
};
template <typename F>
MemoizedFunctor(F)
-> MemoizedFunctor<F,
decltype(getOut(std::declval<F>())),
decltype(getIn(std::declval<F>()))>;
int main ()
{
MemoizedFunctor f{YEqualsX{}};
f(0); // First call
f(0); // Cached call
}
-- EDIT --
Aschepler, in a comment, observed that there is a possible drawback in this solution: some types can't be returned from a function.
By example, a function can't return a C-style array.
This ins't a problem deducing TOut
(the type returned by operator()
) exactly because is a type returned by a method so is returnable also by getOut()
.
But this can be (generally speaking) a problem for TIn
: if TIn
is, by example, int[4]
(can't be, in this case, because is used as a key for an unordered map but, I repeat, generally speaking), a int[4]
can't returned by getIn()
.
You can go around this problem (1) adding a type wrapper struct as follows
template <typename T>
struct typeWrapper
{ using type = T; };
(2) modifying getIn()
to return the wrapper TIn
template <typename TOut, typename TIn>
constexpr typeWrapper<TIn> getIn (Functor<TOut, TIn> const &);
and (3) modifying the deduction guide to extract TIn
from the wrapper
template <typename F>
MemoizedFunctor(F)
-> MemoizedFunctor<F,
decltype(getOut(std::declval<F>())),
typename decltype(getIn(std::declval<F>()))::type>;
Upvotes: 4
Reputation: 303147
In:
template <typename F, typename TOut, typename TIn>
class MemoizedFunctor : public Functor<TOut, TIn> { ... }
If F
must be an implementation of Functor
(which I'm guessing is the case?), you can add a few aliases to Functor
to make your life easier (these can be done externally instead of internally, but it seems reasonable to make this intrusive):
template <typename TOut, typename TIn>
class Functor {
public:
using in_param = TIn;
using out_param = TOut;
// ... rest as before ...
};
And then change MemoizedFunctor
to use those aliases directly. You don't really have independent template parameters, they're completely dependent right?
template <typename F>
class MemoizedFunctor : public Functor<typename F::out_param, typename F::in_param> { ... }
With that change (and likewise change your interal use of TOut
and TIn
), this works as desired (since, of course, we only have one template parameter now so there's only one to provide):
MemoizedFunctor<YEqualsX> f(YEqualsX{});
And that parameter can be deduced through CTAD directly without any further changes
MemoizedFunctor f(YEqualsX{});
Upvotes: 4