从另一个类模板中派生的类模板类型推导

3

我正在尝试编写一个函数对象存储器,以节省重复昂贵的函数调用所需的时间。在我的类设计中,我正在努力寻找一个简单的接口。

使用这个函数对象基类:

template <typename TOut, typename TIn>
class Functor {
public:
    virtual
    ~Functor() {
    }

    virtual
    TOut operator()(TIn input) = 0;
};

我现在想编写一个类,用于封装和记忆一个函数对象。除了封装一个 FunctorMemoizedFunctor 本身也将是一个 Functor。因此,需要有 3 个模板参数。

下面是一个可行的示例:

#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;
}

我觉得一定有一种方法可以消除指定所有3个模板参数的必要性。考虑到传递给MemoizedFunctor构造函数的函数,我认为这三个模板参数都可以被推断出来。

我不确定如何重写这个类,以便使用它不需要所有的模板规范。

我尝试在MemoizedFunctor中使用一个智能指针作为成员变量。这消除了第一个模板参数,但现在类的使用者必须将一个智能指针传递给MemoizedFunctor类。

总之,我希望在构造时自动推导出MemoizedFunctor的所有模板参数。我相信这是可能的,因为在构造时所有模板参数都是明确的。


1
你把输入/输出参数搞反了 - 能不能选择一个一致的顺序? - Barry
@Barry 不好意思,希望现在已经修复了。从一台机器转移到另一台机器时出现了错误。 - Pavis11
2个回答

4
总结一下,我希望在构造时自动推导MemoizedFunctor的所有模板参数。我相信这是可能的,因为在构造时所有模板参数都是明确的。
如果我理解正确,MemoizedFunctor的第一个模板类型始终是Functor或某些从某个Functior继承的东西,其中TOut和TIn是MemoizedFunctor的第二个和第三个模板参数。
在我看来,您正在寻找一个推导指南。
为了推断第二个和第三个模板参数,我建议声明以下一对函数(不需要定义,因为仅在decltype()中使用)
template <typename TOut, typename TIn>
constexpr TIn getIn (Functor<TOut, TIn> const &);

template <typename TOut, typename TIn>
constexpr TOut getOut (Functor<TOut, TIn> const &);

现在,使用 decltype()std::declval(),用户定义的推导指南变得更加简单。
template <typename F>
MemoizedFunctor(F)
   -> MemoizedFunctor<F,
                      decltype(getOut(std::declval<F>())),
                      decltype(getIn(std::declval<F>()))>;

下面是一个完整的编译示例。
#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
 }

-- 编辑 --

Aschepler在评论中指出这种解决方案可能存在一个缺点:有些类型无法从函数返回。

例如,函数无法返回C风格的数组。

这不是一个问题,因为可以通过方法返回的类型来推导TOut(由operator()返回的类型)。因此,getOut()也可以返回该类型。

但是,对于TIn而言,这通常是一个问题:如果TInint[4](在这种情况下不能这样做,因为它用作无序映射的键,但我重申,通常而言),则getIn()无法返回int[4]

您可以通过(1)添加一个类型包装结构来解决这个问题,具体如下:

template <typename T>
struct typeWrapper
 { using type = T; };

(2) 修改 getIn() 方法,使其返回包装器 TIn

template <typename TOut, typename TIn>
constexpr typeWrapper<TIn> getIn (Functor<TOut, TIn> const &);

并且(3)修改扣除指南以从包装器中提取TIn

template <typename F>
MemoizedFunctor(F)
   -> MemoizedFunctor<F,
                      decltype(getOut(std::declval<F>())),
                      typename decltype(getIn(std::declval<F>()))::type>;

非常感谢您的回答!这看起来正是我所希望实现的。明天我会仔细研究它,以便全面理解。 - Pavis11
2
这种函数声明方法的一个缺点是,某些 C++ 类型(原始数组、抽象类、函数类型)永远不能作为函数返回类型。对于这个例子来说并不是问题,因为 Functor<TOut, TIn>::operator() 已经将它们用作了返回类型和参数类型。 - aschepler
1
@aschepler - 很好的观点;对于TOut来说肯定不是问题,因为它是operator()的返回类型,所以可以返回;但是对于TIn来说可能是个问题:如果TIn是(例如)int [4]operator()可以接收它,但是getIn()无法返回它。嗯...也许我会添加一个警告。 - max66
1
@aschepler - 仔细思考了一下这个问题,有一个简单的解决方案:从getIn()返回TIn类型,但将其包装在一个结构中。感谢指出这个问题。 - max66
1
@Pavis11 - aschepler 指出了我的解决方案可能存在问题;我修改了答案以绕过它(但也请看一下 Barry 的解决方案:如果你可以在 Functor 基类中添加一对 using 别名,一切都会变得更简单)。 - max66

4

输入:

template <typename F, typename TOut, typename TIn>
class MemoizedFunctor : public Functor<TOut, TIn> { ... }

如果 F 必须Functor 接口的实现(我猜是这种情况?),你可以为 Functor 添加一些别名,以使你的生活更轻松(这些别名可以在外部完成,而不是内部完成,但在内部完成似乎更为合理):
template <typename TOut, typename TIn>
class Functor {
public:
    using in_param = TIn;
    using out_param = TOut;
    // ... rest as before ...
};

然后将 MemoizedFunctor 更改为直接使用这些别名。你并没有真正独立的模板参数,它们完全是依赖关系对吧?

template <typename F>
class MemoizedFunctor : public Functor<typename F::out_param, typename F::in_param> { ... }

通过这个更改(同样更改您对 TOutTIn 的内部使用),此代码现在可以按预期工作(因为现在我们只有一个模板参数,所以只需要提供一个即可):

MemoizedFunctor<YEqualsX> f(YEqualsX{});

而且该参数可以通过CTAD直接推导出来,无需进行任何进一步的更改

MemoizedFunctor f(YEqualsX{});

你的假设 F 也必须是 Functor 的实现是正确的。我认为类似这样的解决方案一定存在,因为正如你所说,我并没有真正拥有3个独立的模板参数。我实际上尝试过这个解决方案,但没有使用别名 (class MemoizedFunctor : public Functor<typename F::TOut, typename F::TIn>),当然这并没有起作用。我知道这个语法看起来很奇怪,而这些别名似乎就是解决方案。 - Pavis11

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接