用于以通用方式返回序列的 C++ API
Posted
技术标签:
【中文标题】用于以通用方式返回序列的 C++ API【英文标题】:C++ API for returning sequences in a generic way 【发布时间】:2010-09-10 23:29:48 【问题描述】:如果我正在编写一个库并且我有一个需要返回一系列值的函数,我可以这样做:
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)
创建),但这似乎太容易误用,因为编译器很乐意接受非插入迭代器,但在运行时会表现不正确。
那么,设计返回任意长度序列的通用接口是否有最佳实践?
【问题讨论】:
【参考方案1】:让 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
【讨论】:
这是一个有趣的解决方案,但是,它不适用于某些情况。如果您将整个序列缓存在类中,或者如果在序列中生成单个项目很便宜,它将很好地工作。否则效率会很低。 单独的前向迭代器并不能为用户提供检查序列结尾的方法。但是返回一个范围是可行的(即一个迭代器对给出begin
和end
)。
这是最好的解决方案:提供begin()
和end()
函数以返回您自己的iterator
。如果您没有缓存整个序列并且生成单个项目并不便宜,那么您必须在迭代器类型中添加额外信息以简化单个项目的生成。如果有帮助,迭代器可以不仅仅是指针。【参考方案2】:
呃...只是我的两分钱,但是:
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
但正如其他帖子所说,您应该接受将界面仅限于一种容器。
【讨论】:
【参考方案3】:为什么您的界面需要独立于容器? Scott Meyers 在他的“Effective STL”中给出了一个很好的理由:不要试图让你的代码独立于容器,不管诱惑有多大。基本上,容器的用途完全不同:您可能不想将输出存储在 map 或 set 中(它们不是间隔容器),因此您只剩下 vector、list 和 deque,以及您为什么希望在需要列表的地方有向量,反之亦然?它们是完全不同的,使用其中一个的所有功能,你会得到比试图同时工作更好的结果。好吧,请考虑阅读“Effective 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);
//...
【讨论】:
当然,我同意基本观点,但标准库并不是容器类的唯一提供者。例如,将库与 Qt 结合使用的人可能希望使用 QVector 而不是 std::vector 等。我的目标并不是真正支持完全不同类型的容器。 当然,我同意你的基本观点,但我真的希望 Qt 的人能开始使用 std。 /我笑了。【参考方案4】:需要特别注意的一件事是,如果您在 library 中指的是 DLL 或类似的东西。如果库使用者(例如应用程序)是使用库本身以外的其他编译器构建的,则可能会出现问题。
考虑您在示例中按值返回 std::vector<>
的情况。然后内存将在库的上下文中分配,但在应用程序的上下文中释放。两个不同的编译器可能会以不同的方式分配/解除分配,因此可能会发生严重破坏。
【讨论】:
有点题外话:在 C++ 中,您很有可能保证您正在编写的库将使用用于客户端应用程序的相同编译器进行编译,或者不会暴露模板化或内联界面中的代码(在过去的一些工作中,我们为我们编写的每个库生成了三个二进制文件,每个需要支持的编译器一个)。【参考方案5】:如果您已经为序列管理内存,则可以返回一对迭代器供调用者在 for 循环或算法调用中使用。
如果返回的序列需要管理自己的内存,那么事情就更复杂了。您可以使用@paercebal 的解决方案,也可以实现自己的迭代器,将 shared_ptr 保存到它们正在迭代的序列中。
【讨论】:
【参考方案6】:std::list<int>
稍微好一点,IMO。请注意,这不需要列表中数据的额外副本,因为它只是被复制的指针。
这完全取决于您的消费者。如果您可以期望他们成为 C++ 开发人员,请给他们一个 std
容器类,我说。
我唯一想到的另一件事是你会这样做:
void get_sequence(std::tr1::function<void(int)> f);
然后调用者可以使用std::tr1::bind
让您的get_sequence
函数调用他们想要的任何对象(或不)上的任何函数。您只需为您正在创建的每个元素继续调用f
。
【讨论】:
vector
也只包含一个指向数据的指针,并且仍然必须复制整个内容以防止混叠。列表也是如此。此外,为什么它“更好,IMO”?这一点都不主观!列表在中间需要快速插入和删除时非常适合,基本上没有别的用处。【参考方案7】:
你可以这样做
template<typename container>
container get_sequence();
并要求提供的容器类型符合一些标准接口(比如有一个成员 push_back 并且可能保留,以便接口的用户可以使用 vector/deque/list)。
【讨论】:
【参考方案8】:您可以使用 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);
【讨论】:
【参考方案9】:对于输出序列,我看到了两个选项。第一个类似于
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
,结合一些按顺序返回整数的迭代器。
【讨论】:
第二个只不过是具有另一种语法的 C++ 迭代器。为什么不使用既定的语法? 因为很难做到正确。但我提到了boost::iterator_facade
。【参考方案10】:
您可以将仿函数传递给接受单个值的函数。然后,仿函数将负责将值存储在您当时使用的任何容器中。
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)->voidui.push_back(n););
(我仍在使用 VS2008,所以我不确定我的 lambda 语法是否正确)
【讨论】:
以上是关于用于以通用方式返回序列的 C++ API的主要内容,如果未能解决你的问题,请参考以下文章