6

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

std::vector<int> get_sequence();

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

理论上,您可以通过制作一个带有开始和结束迭代器的模板化函数来启用任意容器的使用(并避免不必要的额外复制):

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

然后该函数会将序列值存储在迭代器给定的范围内。但是这样做的问题是它需要您知道序列的大小,以便在序列之间有足够的元素beginend存储序列中的所有值。

我想到了一个界面,例如:

template<T_insertIter> get_sequence(T_insertIter inserter);

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

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

4

10 回答 10

6

让 get_sequence 返回一个(自定义)forward_iterator类,按需生成序列。(它也可以是更高级的迭代器类型,例如bidirectional_iterator这对您的序列是否实用。)

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

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

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

where...表示创建结束迭代器(可能使用私有构造函数)所需的任何参数。然后你的循环看起来像:

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
于 2008-09-18T22:23:15.970 回答
3

为什么你需要你的接口独立于容器?Scott Meyers 在他的“Effective STL”中给出了一个很好的理由,即不管诱惑有多大,不要试图让你的代码独立于容器。基本上,容器旨在用于完全不同的用途:您可能不想将输出存储在 map 或 set 中(它们不是间隔容器),因此您只剩下 vector、list 和 deque,以及您为什么希望在需要列表的地方有向量,反之亦然?它们是完全不同的,使用其中一个的所有功能将获得比尝试同时工作更好的结果。好吧,请考虑阅读“有效的 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);
  //...
}
于 2008-09-18T22:14:00.157 回答
3

呃......只是我的两分钱,但是:

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
}

但正如其他帖子所说,您应该接受将您的界面仅限于一种容器。

于 2008-09-18T22:38:56.447 回答
2

需要特别注意的一件事是,如果您所说的是指 DLL 或类似的。如果库使用者(例如应用程序)是用另一个编译器而不是库本身构建的,那么可能会出现问题。

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

于 2008-09-18T22:24:11.863 回答
1

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

如果返回的序列需要管理自己的内存,那么事情就更复杂了。您可以使用@paercebal 的解决方案,或者您可以实现自己的迭代器,将 shared_ptr 保存到它们正在迭代的序列中。

于 2008-09-19T00:49:14.807 回答
0

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

这完全取决于您的消费者。如果您可以期望他们成为 C++ 开发人员,请给他们一个std容器类,我说。

我唯一想到的另一件事是你会这样做:

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

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

于 2008-09-18T22:10:20.307 回答
0

你可以做类似的事情

template<typename container>
container get_sequence();

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

于 2008-09-18T22:13:03.420 回答
0

您可以使用 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);
于 2008-09-18T22:19:30.377 回答
0

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

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 来定义函子参数。你不需要提升就能做到这一点。它只是让它更简单。

您也可以使用函数指针而不是仿函数,但我不推荐它。它容易出错,并且没有简单的方法将数据绑定到它。

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

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

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

于 2010-09-17T15:01:47.967 回答
0

对于输出序列,我看到两个选项。第一个是类似的

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, 结合一些按顺序返回整数的迭代器。

于 2010-09-17T14:47:08.143 回答