如何在C++中通用地迭代集合?

9

简单来说,如果我有一个集合和向量,如何创建一个通用方法来处理两者作为参数的情况。

我想做的就是迭代这两种类型的集合。听起来应该很简单,但我似乎漏掉了什么。

void printMeSomeStrings(somebaseclass<string> strings) {
  for (auto& str : strings) {
    cout << str << endl;
  }
}

在C#中,我会传递IEnumerable或类似的东西。然后我可以遍历集合。
如有任何一般性阅读解释答案都会受到欣赏。
4个回答

5
您可以使用模板。例如:
#include <iostream>

template<typename C>
void foo(C const& c)
{
    std::cout << "{ ";
    for (auto const& x : c)
    {
        std::cout << x << " ";
    }
    std::cout << "}";
}

以下是如何使用它的示例:

#include <set>
#include <vector>

int main()
{
    std::vector<int> v = {1, 2, 3};
    foo(v);

    std::cout << std::endl;

    std::set<std::string> s = {"Hello,", "Generic", "World!"};
    foo(s);
}

Live example.


一个好的答案是,如果你想避免定义自己的模板,可以使用带有lambda表达式或printer函数的std::for_each。 - Alexander Oh

3
这正是迭代器设计的初衷。
template <class It>
void print_some_strings(It first, It last) {
    while (first != last)
        std::cout << *first++ << '\n';
}

是我看错了还是你没有遍历列表?'last'参数在任何地方都没有被使用。 - Elephantik

2
第一种选择是将迭代代码放入模板中。这需要向使用它的所有人公开实现,具有不利之处。
基本上,将类型 C 作为 template 参数,然后按照该类型 C 编写代码。
template<typename C>
void printMeSomeStrings(C&& strings) {
  for (auto const& str : strings) {
    cout << str << endl;
  }
}

如果你想在接口和实现之间建立强大的屏障,C++11的方法是对可迭代容器进行类型擦除,然后公开一个类似于std::function的可迭代容器。
这有些棘手。我个人认为编写for_each函数比编写完整的迭代适配器更容易。如果你想要完整的容器迭代类型擦除对象,请从boost开始,或者在下面向我提问,我可能会做。
不过,for_each适配器很容易。
#include <functional>
#include <utility>
#include <iterator>
#include <memory>

template<typename T>
struct for_each_helper_interface {
  virtual ~for_each_helper_interface() {}
  virtual void for_each( std::function< void(T) > const& ) = 0;
};
template<typename C, typename T>
struct for_each_helper:for_each_helper_interface<T> {
  C& c;
  for_each_helper( C& in ):c(in) {}
  virtual void for_each( std::function< void(T) > const& f ) override final {
    for( auto&& x:c ) {
      f(x);
    }
  }
};
template<typename T>
struct for_each_adaptor {
  std::unique_ptr<for_each_helper_interface<T>> pImpl;
  void for_each( std::function< void(T) > const& f ) {
    if (pImpl) {
      pImpl->for_each(f);
    }
  }
  template<typename C>
  for_each_adaptor( C&& c ): pImpl( new for_each_helper<C, T>( std::forward<C>(c) ) ) {}
};

这将对T的容器(或可转换为T的类型)进行类型擦除,并公开一个for_each方法,让您可以迭代容器的内容。使用方法如下:

#include <set>
#include <iostream>
#include <vector>
void print_stufF( for_each_adaptor<std::string const&> c ) {
  c.for_each([&](std::string const&s){
    std::cout << s << "\n";
  });
}
int main() {
   std::set<std::string> s;
   s.insert("hello");
   s.insert("world");
   print_stuff(s);
   std::vector<std::string> v;
   v.push_back("hola");
   v.push_back("bola");
   print_stuff(v);
 }

这里发生的情况是,对于用于构建我们的适配器的每种类型,我们都会构建一个自定义的 for each 实现。然后,我们存储指向此自定义类的抽象基类的指针,并将 for each 调用重定向到它。
这意味着任何专门化 std::begin 或定义其自己的 begin 的内容都不需要相关:我们在使用时创建临时关系。
实时示例:http://ideone.com/xOqBkI

1
Boost已经有了any_range,它将执行类似于std::function的类型擦除。 - Paul Fultz II

0
在C#中,我会传递IEnumerable或类似的东西。
C++使用更具有Python风格的方法来定义接口(通常在C++中称为概念),而不是使用继承。要在C++中进行鸭子类型,您可以使用像这样的模板函数:
template<typename C>
void printMeSomeStrings(const C& strings) 
{
    for (const auto& str : strings) 
    {
        cout << str << endl;
    }
}

在Python中,鸭子类型是在运行时完成的,但在C++中是在编译时完成的,因此鸭子类型没有运行时成本,并且所有内容也在编译时进行了检查。

这里有更多关于C++的信息,以帮助查找信息。首先,IEnumerator<T>的等效物是C++中的迭代器。这里是一个关于不同迭代器类别及需要实现哪些内容的页面。由于遗留原因,迭代器是按照C语言指针建模的,这使您可以使用C数组与标准C++算法。

然而,与IEnumerator<T>不同,迭代器必须成对出现。一个迭代器指向开始位置,另一个指向结束位置(即最后一个元素的下一个位置)。因此,在C++中,IEnumerable<T>的等效物被称为范围。在C++11中,范围由两个自由函数begin(T)end(T)定义(也可以实现为成员函数.begin().end())。

通过将概念(也称为接口)定义为两个自由函数,而不是使用继承,可以非侵入式地实现范围。因此,例如,如果您需要使用一些使用C样式链接列表的遗留API进行工作。它们现在可以被适配为C++11范围,并在C++ for循环中使用。

1
这不是鸭子类型 - 这是类型推断。 - Michael Burge
@MichaelBurge 在C++社区中,这通常被称为编译时鸭子类型。请参见此处 - Paul Fultz II

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