是否可以在现代C ++中的虚函数中使用模板参数?

时间:2015-06-18 18:48:06

标签: swift templates c++11 generics architecture

几年前我曾经做过C ++开发,当时我发现很难将模板编程与OOP结合起来。目前我在Swift编程,我尝试做一些我当时努力的事情。

这个Swift代码将说明问题:

// protocol is like Java interface or C++ pure virtual base class
protocol Log {

    // want to able to add elements from a collection of Ints, but
    // it should be any sort of collection that 
    // can be treated as a sequence
    func add<T: SequenceType where T.Generator.Element == Int>(values: T)
}

class DiscreteLog: Log {
    var vals: [Int] = []
    func add<T: SequenceType where T.Generator.Element == Int>(values: T) {
        for v in values {
            vals.append(v)
        }
    }

}

class ContinousLog: Log {
    var vals: [Double] = []

    func add<T: SequenceType where T.Generator.Element == Int>(values: T) {
        for v in values {
            vals.append(Double(v))
        }
    }
}

// I don't have to know whether the log is Continuous or Discrete
// I can still add elements to it
var log: Log = ContinousLog()
log.add([1, 2, 3])

// and elements can come from any kind of sequence, it does not need
// to be an array
log.add(["four": 4, "five: 5].values)

所以问题是如果C ++代码定义为:

virtual void add(vector<Int> elements>)

然后确定我可以有多个子类实现这个方法,但我永远不会提供任何东西,只有vector作为参数。

我可以尝试使用iterator将其更改为更通用的东西:

virtual void add(vector<Int>::iterator elements>)

但我仍然局限于使用矢量迭代器。所以我想我必须写一些像:

template<typename Iterator>
virtual void add(Iterator elements>)

但是这会产生编译错误,因为虚拟方法不允许基于模板的参数。

无论如何,我想知道在现代C ++中这种事情是否可行。

1 个答案:

答案 0 :(得分:0)

C ++模板和C#/ Swift / Java泛型是不同的东西。

他们都是&#34;模式代码&#34;从某种意义上说(它们是生成代码的模式),但C#/ Swift / Java泛型使用类型擦除和“忘记”#34;几乎所有关于他们使用的类型的东西,而C ++模板是大象。大象永远不会忘记。

事实证明,可以让大象忘记,但你必须告诉它。 &#34;忘记&#34;的技术关于类型的细节被称为&#34;类型擦除&#34;或者&#34;运行时间概念&#34;。

所以你想要删除&#34;整数序列&#34;的概念。你想要采用任何类型,只要它是一个整数序列,并且能够迭代它。看似公平。

boost有这种类型的删除。但谁愿意总是依靠提升?

首先,键入erase an input iterator:

  template<class T>
  struct input_iterator:
    std::iterator<
      std::input_iterator_tag, // category
      T, // value
      std::ptrdiff_t, // distance
      T*, // pointer
      T // reference
    >
  {
    struct erase {
      virtual void advance() = 0;
      virtual erase* clone() const = 0;
      virtual T get() const = 0;
      virtual bool equal(erase const& o) = 0;
      virtual ~erase() {}
    };
    std::unique_ptr<erase> pimpl;
    input_iterator(input_iterator&&)=default;
    input_iterator& operator=(input_iterator&&)=default;
    input_iterator()=default;
    input_iterator& operator++() {
      pimpl->advance();
      return *this;
    }
    input_iterator operator++(int) {
      auto copy = *this;
      ++*this;
      return copy;
    }
    input_iterator(input_iterator const& o):
      pimpl(o.pimpl?o.pimpl->clone():nullptr)
    {}
    input_iterator& operator=(input_iterator const&o) {
      if (!o.pimpl) {
        if (pimpl) pimpl->reset();
        return *this;
      }
      pimpl = std::unique_ptr<erase>(o.pimpl->clone());
      return *this;
    }
    T operator*() const {
      return pimpl->get();
    }
    friend bool operator==( input_iterator const& lhs, input_iterator const& rhs ) {
      return lhs.pimpl->equal(*rhs.pimpl);
    }
    friend bool operator!=( input_iterator const& lhs, input_iterator const& rhs ) {
      return !(lhs==rhs);
    }
    template<class It>
    struct impl:erase{
      It it;
      impl(impl const&)=default;
      impl(It in):it(std::move(in)){}
      virtual void advance() override { ++it; }
      virtual erase* clone() const override { return new impl(*this); }
      virtual T get() const override { return *it; }
      virtual bool equal(erase const& o) override {
        return static_cast<impl const&>(o).it == it;
      }
    };

    template<
      class It,
      class=std::enable_if<
        std::is_convertible<
          typename std::iterator_traits<It>::reference,
          T
        >{}
      >
    >
    input_iterator(It it):pimpl( new impl<It>{it} ) {}
  }; // input_iterator

接下来,有一个范围模板。这是一个存储非类型擦除迭代器的容器,并且公开足以迭代这些迭代器。

 template<class It>
 struct range {
   It b; It e;
   It begin() const { return b; }
   It end() const { return e; }

   range() = default;

   range(It start, It finish):b(std::move(start)),e(std::move(finish)) {};

   range(range&&)=default;
   range(range const&)=default;
   range& operator=(range&&)=default;
   range& operator=(range const&)=default;

   template<class R,
     class R_It=std::decay_t<decltype(std::begin(std::declval<R>()))>,
     class=std::enable_if< std::is_convertible<R_It, It>{} >
   >
   range( R&& r ):
     range(std::begin(r), std::end(r))
   {} // TODO: enable ADL begin lookup
 };

上面的类型非常基本:C ++ 1z和boost都有更好的类型,就像我自己的代码库中一样。但它足以处理for(:)循环,以及从具有兼容迭代器的容器进行隐式转换。

最后我们的序列类型:

template<class T>
using sequence_of = range<input_iterator<T>>;

等等,那是吗?很好,那些类型构成很好!

除非出现错误,否则我们已经完成了。

您的代码现在需要sequence_of<int>,他们可以传递std::vector<int>std::list<int>或其他任何内容。

input_iterator类型 - 擦除类型 - 删除所有迭代器,直到通过T*,复制和==前进获得++,这对于a for(:)循环。

range<input_iterator<int>>将接受其迭代器可以转换为input_iterator<int>的任何可迭代范围(包括容器)。

缺点?我们刚刚介绍了一堆开销。每种方法都经过虚拟调度,从++*再到==

这是(粗略地)泛型所做的事情 - 他们键入 - 删除你在泛型子句中给出的要求。这意味着他们正在处理抽象对象,而不是具体对象,所以他们不可避免地会受到这种间接性能的惩罚。

C ++模板可用于生成类型擦除,甚至还有工具(boost有一些)可以使它更容易。我上面做的是半手动的。类似的技术在std::function<R(Args...)>中使用,其类型删除到(概念上){copy,call with(Args ...)return R,destroy}(加上一些杂项)。

live example

(上面的代码自由使用C ++ 14。)

所以C ++等价物Log是:

struct Log {
  virtual void add(sequence_of<int>) = 0;
  virtual ~Log() {}
};

现在,上面的类型擦除代码有点难看。公平地说,我只是在C ++中实现了一种语言功能而没有直接的语言支持。

我已经看到了一些在C ++中使类型擦除更容易的建议。我不知道这些提案的状态。

如果你想自己动手,这里就是一个简单的&#34;通过3个步骤进行类型擦除的方法:

首先,确定要删除的操作。写相当于input_iterator<T> - 给它做一堆你想要的方法和操作符。稀疏。将其称为&#34;外部类型&#34;。理想情况下,此类型中的 nothing virtual,它应该是常规或半规则类型(即,它应该像值一样,或者只移动值)。除了界面之外,别实施任何东西。

其次,写一个内部类erase。它为一组功能提供纯虚拟接口,可以提供外部类型所需的功能。

在外部类型中存储unique_ptr<erase> pimpl;。将您在外部类型中公开的方法转发到pimpl;

第三,写一个内部template<class X> class impl<X>:erase。它存储变量X x;,并通过与erase交互来实现X中的所有内容。它应该可以从X构建(可选的完美转发)。

然后,您可以为通过pimpl创建其new impl<X>(whatever)的外部类型创建一个完美的转发构造函数。理想情况下,它应该通过SFINAE技术检查其参数是否有效,但这只是实现问题的合格。

现在外部类型&#34;擦除&#34;它构造的任何对象的类型&#34;到#34;你暴露的行动。

现在,对于您的实际问题,我写array_view或窃取std::experimental::array_view,并将我的输入限制为该类型数据的任何类型的连续缓冲区。这是更高效的,除非你确实需要,否则接受任何序列都是过度工程的。