用于以通用方式返回序列的C ++ API

时间:2020-03-06 14:23:44  来源:igfitidea点击:

如果我正在编写一个库,并且我有一个需要返回值序列的函数,则可以执行以下操作:

std::vector<int> get_sequence();

但是,这要求库用户使用std :: vector <>容器,而不是允许他们使用要使用的任何容器。此外,它可以添加返回数组的额外副本(取决于编译器是否可以对此进行优化),这可能会对性能产生负面影响。

从理论上讲,我们可以通过使模板函数开始和结束迭代来启用任意容器的使用(并避免不必要的额外复制):

template<class T_iter> void get_sequence(T_iter begin, T_iter end);

然后该函数将序列值存储在迭代器给定的范围内。但是这样做的问题是,它要求我们知道序列的大小,以便在" begin"和" end"之间有足够的元素来存储序列中的所有值。

我考虑过一个接口,例如:

template<T_insertIter> get_sequence(T_insertIter inserter);

这要求T_insertIter是一个插入迭代器(例如,使用std :: back_inserter(my_vector)创建),但是这种方法似乎很容易被滥用,因为编译器会很乐意接受非插入迭代器,但是在运行时行为不正确。时间。

那么,是否存在设计返回任意长度序列的通用接口的最佳实践?

解决方案

IMO,std :: list &lt;int>稍微好一点。请注意,这将不需要列表中数据的额外副本,因为它仅是指针的复制。

这完全取决于消费者。如果我们可以期望他们是C ++开发人员,请给他们提供std容器类之一。

对我而言,发生的唯一另一件事是我们将执行此操作:

void get_sequence(std::tr1::function<void(int)> f);

然后调用者可以使用std :: tr1 :: bind使get_sequence函数在他们想要的对象(或者不对象)上调用任何函数。我们只需为要创建的每个元素继续调用f

你可以做类似的事情

template<typename container>
container get_sequence();

并要求提供的容器类型符合某些标准接口(例如具有成员push_back并可能保留,以便我们接口的用户可以使用vector / deque / list)。

为什么需要接口独立于容器?斯科特·迈耶斯(Scott Meyers)在其"有效的STL"中给出了一个很好的理由,无论尝试多么强大,都不要使代码独立于容器。基本上,容器旨在用于完全不同的用途:我们可能不希望将输出存储在map或者set中(它们不是间隔容器),所以剩下矢量,列表和双端队列,为什么要这么做在需要列表的地方有矢量,反之亦然?它们是完全不同的,并且使用它们之一的所有功能都比使两者同时工作要好。好吧,只需考虑阅读"有效的STL":值得我们花时间。

不过,如果我们对容器有所了解,可以考虑做类似的事情

template void get_sequence(T_Container & container)
{
  //...
  container.assign(iter1, iter2);
  //...
}

或者许

template void get_sequence(T_Container & container)
{
  //...
  container.resize(size);
  //use push_back or whatever
  //...
}

甚至控制我们用策略做的事,例如

class AssignStrategy // for stl
{
public:
  template
  void fill(T_Container & container, T_Container::iterator it1, T_Container::iterator it2){
    container.assign(it1, it2);
  }
};

class ReserveStrategy // for vectors and stuff
{
public:
  template
  void fill(T_Container & container, T_Container::iterator it1, T_Container::iterator it2){
    container.reserve(it2 - it1);
    while(it1 != it2)
      container.push_back(*it1++);
  }
};

template 
void get_sequence(T_Container & container)
{
  //...
  T_FillStrategy::fill(container, iter1, iter2);
  //...
}

我们可以使用iterator_traits静态调度迭代器的类型

像这样的东西:

template<T_insertIter> get_sequence(T_insertIter inserter)
{
   return get_sequence(inserter, typename iterator_traits<Iterator>::iterator_category());
}

template<T_insertIter> get_sequence(T_insertIter inserter, input_iterator_tag);

让get_sequence返回一个(自定义的)" forward_iterator"类,该类按需生成序列。 (如果序列很实际的话,它也可以是更高级的迭代器类型,例如bidirectional_iterator。)

然后,用户可以将序列复制到所需的任何容器类型中。或者,它们可以直接在迭代器上循环并完全跳过容器。

我们将需要某种结束迭代器。在不确切知道如何生成序列的情况下,很难确切说明如何实现该序列。一种方法是让迭代器类具有返回结束迭代器的静态成员函数,例如:

static const my_itr& end() { static const my_itr e(...); return e; };

其中" ..."代表创建最终迭代器(可能使用私有构造函数)所需的任何参数。然后,循环将如下所示:

for (my_itr i = get_sequence(); i != my_itr::end(); ++i) { ... }

这是一个正向迭代器类的简单示例,该类生成一系列连续的整数。显然,可以很容易地将其转换为双向或者随机访问迭代器,但是我想使示例保持较小。

#include <iterator>

class integer_sequence_itr
  : public std::iterator<std::forward_iterator_tag, int>
{
 private:
  int i;

 public:
  explicit integer_sequence_itr(int start) : i(start) {};

  const int& operator*()  const { return i; };
  const int* operator->() const { return &i; };

  integer_sequence_itr& operator++() { ++i; return *this; };
  integer_sequence_itr  operator++(int)
    { integer_sequence_itr copy(*this); ++i; return copy; };

  inline bool operator==(const integer_sequence_itr& rhs) const
    { return i == rhs.i; };

  inline bool operator!=(const integer_sequence_itr& rhs) const
    { return i != rhs.i; };
}; // end integer_sequence_itr

//Example:  Print the integers from 1 to 10.
#include <iostream>

int main()
{
  const integer_sequence_itr stop(11);

  for (integer_sequence_itr i(1); i != stop; ++i)
    std::cout << *i << std::endl;

  return 0;
} // end main

要特别注意的一件事是,如果我们使用库表示DLL或者类似文件。如果库使用者(例如应用程序)是使用库本身之外的其他编译器构建的,则可能会出现问题。

考虑在示例中按值返回std :: vector &lt;>的情况。然后,将在库的上下文中分配内存,但在应用程序的上下文中释放内存。两个不同的编译器可能会以不同的方式分配/解除分配,因此可能会造成严重破坏。

差……我的两分钱,但是:

void get_sequence(std::vector<int> & p_aInt);

这将消除潜在的按副本退货问题。
现在,如果我们真的想避免强加容器,则可以尝试执行以下操作:

template <typename T>
void get_sequence(T & p_aInt)
{
    p_aInt.push_back(25) ; // Or whatever you need to add
}

这只会针对矢量,列表和双端队列(以及类似的容器)进行编译。如果我们需要尽可能多的可能的容器集,则代码为:

template <typename T>
void get_sequence(T & p_aInt)
{
    p_aInt.insert(p_aInt.end(), 25) ; // Or whatever you need to add
}

但是正如其他帖子所述,我们应该接受将接口限制为仅一种容器。

如果我们已经为序列管理内存,则可以返回一对迭代器,以供调用方在for循环或者算法调用中使用。

如果返回的序列需要管理自己的内存,那么事情就会更加复杂。我们可以使用@paercebal的解决方案,也可以实现自己的迭代器,以将shared_ptr保留为其迭代的顺序。

对于输出序列,我看到两个选项。首先是这样的

template <typename OutputIter>
void generate_sequence(OutputIter out)
{
    //...
    while (...) { *out = ...; ++out; }
}

第二个是这样的

struct sequence_generator
{
    bool has_next() { ... }
    your_type next() { mutate_state(); return next_value; }

private:
    // some state
};

我们可能希望将其转换为标准的C ++迭代器(为了方便起见使用boost :: iterator_facade)以将其用于标准算法(copy,`transform,...)。

还要看看boost :: transform_iterator,并结合一些迭代器,这些迭代器按顺序返回整数。

我们可以将函子传递给接受单个值的函数。然后,函子将负责将值存储在我们当时使用的任何容器中。

struct vector_adder {
  vector_adder(std::vector<int>& v) : v(v) {}
  void operator()(int n) { v.push_back(n); }
  std::vector<int>& v;
};

void gen_sequence(boost::function< void(int) > f) {
  ...
  f(n);
  ...
}

main() {
  std::vector<int> vi;
  gen_sequence(vector_adder(vi));
}

注意:我在这里使用boost.function来定义functor参数。我们不需要增强就可以做到这一点。它只是使它变得简单得多。

我们也可以使用函数指针代替函子,但我不建议这样做。这容易出错,没有简单的方法将数据绑定到它。

另外,如果编译器支持C ++ 0x lambda函数,则可以通过消除显式的functor定义来简化代码:

main() {
  std::vector<int> ui;
  gen_sequence([&](int n)->void{ui.push_back(n);});
}

(我仍在使用VS2008,所以不确定我是否正确使用了lambda语法)