Qual é a melhor maneira de iterar em dois ou mais contêineres simultaneamente

114

C ++ 11 oferece várias maneiras de iterar em contêineres. Por exemplo:

Loop baseado em alcance

for(auto c : container) fun(c)

std :: for_each

for_each(container.begin(),container.end(),fun)

No entanto, qual é a maneira recomendada de iterar em dois (ou mais) contêineres do mesmo tamanho para realizar algo como:

for(unsigned i = 0; i < containerA.size(); ++i) {
  containerA[i] = containerB[i];
}
memecs
fonte
1
que tal transformapresentar em #include <algorithm>?
Ankit Acharya
Sobre o loop de atribuição: se ambos forem vetores ou similares, use ao containerA = containerB;invés do loop.
emlai
Uma pergunta semelhante: stackoverflow.com/questions/8511035/…
knedlsepp
1
Possível duplicata da função Sequence-zip para c ++ 11?
sublinhado_d

Respostas:

53

Um pouco tarde para a festa. Mas: eu iria iterar sobre os índices. Mas não com o forloop clássico, mas sim com um forloop baseado em intervalo sobre os índices:

for(unsigned i : indices(containerA)) {
    containerA[i] = containerB[i];
}

indicesé uma função de invólucro simples que retorna um intervalo (avaliado lentamente) para os índices. Como a implementação - embora simples - é um pouco longa para postá-la aqui, você pode encontrar uma implementação no GitHub .

Este código é tão eficiente quanto usar um forloop clássico manual .

Se esse padrão ocorrer com frequência em seus dados, considere usar outro padrão que seja zipduas sequências e produza uma gama de tuplas, correspondendo aos elementos emparelhados:

for (auto& [a, b] : zip(containerA, containerB)) {
    a = b;
}

A implementação de zipé deixada como um exercício para o leitor, mas decorre facilmente da implementação de indices.

(Antes do C ++ 17, você teria que escrever o seguinte :)

for (auto items&& : zip(containerA, containerB))
    get<0>(items) = get<1>(items);
Konrad Rudolph
fonte
2
Existe alguma vantagem na implementação de seus índices em comparação ao aumento do intervalo de contagem? Pode-se simplesmente usarboost::counting_range(size_t(0), containerA.size())
SebastianK
3
@SebastianK A maior diferença neste caso é a sintaxe: o meu é (eu afirmo) objetivamente melhor para usar neste caso. Além disso, você pode especificar um tamanho de passo. Veja o link da página do Github, e em particular o arquivo README, para exemplos.
Konrad Rudolph
Sua ideia é muito boa e eu só usei o intervalo de contagem depois de vê-lo: clear upvote :) No entanto, eu me pergunto se ele fornece valor adicional para (re) implementar isso. Por exemplo, em relação ao desempenho. Sintaxe melhor, concordo, é claro, mas seria suficiente escrever uma função geradora simples para compensar essa desvantagem.
SebastianK
@SebastianK Admito que, quando escrevi o código, considerei simples o suficiente para viver em isolamento sem usar uma biblioteca (e é!). Agora provavelmente o escreveria como um wrapper em torno de Boost.Range. Dito isso, o desempenho da minha biblioteca já é ótimo. O que quero dizer com isso é que usar minha indicesimplementação produz uma saída do compilador que é idêntica ao uso de forloops manuais . Não há sobrecarga alguma.
Konrad Rudolph
Já que eu uso boost de qualquer maneira, seria mais simples no meu caso. Já escrevi este wrapper em torno do intervalo de aumento: uma função com uma linha de código é tudo que preciso. No entanto, eu estaria interessado se o desempenho dos intervalos de aumento também fosse ideal.
SebastianK
38

Para seu exemplo específico, basta usar

std::copy_n(contB.begin(), contA.size(), contA.begin())

Para o caso mais geral, você pode usar Boost.Iterator's zip_iterator, com uma pequena função para torná-lo utilizável em loops for baseados em intervalo. Na maioria dos casos, isso funcionará:

template<class... Conts>
auto zip_range(Conts&... conts)
  -> decltype(boost::make_iterator_range(
  boost::make_zip_iterator(boost::make_tuple(conts.begin()...)),
  boost::make_zip_iterator(boost::make_tuple(conts.end()...))))
{
  return {boost::make_zip_iterator(boost::make_tuple(conts.begin()...)),
          boost::make_zip_iterator(boost::make_tuple(conts.end()...))};
}

// ...
for(auto&& t : zip_range(contA, contB))
  std::cout << t.get<0>() << " : " << t.get<1>() << "\n";

Exemplo ao vivo.

No entanto, para genericidade full-blown, você provavelmente vai querer algo mais parecido com este , que irá funcionar correctamente para arrays e tipos definidos pelo usuário que não têm membro begin()/ end()mas não têm begin/ endfunções em seu namespace. Além disso, isso permitirá que o usuário obtenha constacesso especificamente por meio das zip_c...funções.

E se você é um defensor de boas mensagens de erro, como eu, então provavelmente você quer isso , que verifica se algum contêiner temporário foi passado para alguma das zip_...funções e imprime uma boa mensagem de erro em caso afirmativo.

Xeo
fonte
1
Obrigado! Uma pergunta, porém, por que você usa auto &&, o que significa &&?
memecs
@memecs: Eu recomendo a leitura desta questão , bem como esta minha resposta que meio que explica como a dedução e o colapso de referência são feitos. Observe que autofunciona exatamente da mesma forma que um parâmetro de modelo e, T&&em um modelo, é uma referência universal, conforme explicado no primeiro link, portanto, auto&& v = 42será deduzido como int&&e auto&& w = v;, em seguida, será deduzido como int&. Ele permite que você combine lvalues, bem como rvalues, e permite que ambos sejam mutáveis, sem fazer uma cópia.
Xeo
@Xeo: Mas qual é a vantagem do auto && sobre o auto e em um loop foreach?
Viktor Sehr
@ViktorSehr: Permite vincular a elementos temporários, como aqueles produzidos por zip_range.
Xeo
23
@Xeo Todos os links para os exemplos estão quebrados.
kynan
34

Eu me pergunto por que ninguém mencionou isso:

auto ItA = VectorA.begin();
auto ItB = VectorB.begin();

while(ItA != VectorA.end() || ItB != VectorB.end())
{
    if(ItA != VectorA.end())
    {
        ++ItA;
    }
    if(ItB != VectorB.end())
    {
        ++ItB;
    }
}

PS: se os tamanhos do contêiner não corresponderem, você terá que colocar o código dentro das instruções if.

Joseph
fonte
9

Existem várias maneiras de fazer coisas específicas com vários contêineres, conforme fornecido no algorithmcabeçalho. Por exemplo, no exemplo que você deu, você poderia usar em std::copyvez de um loop for explícito.

Por outro lado, não há nenhuma maneira integrada de iterar genericamente vários contêineres além de um loop for normal. Isso não é surpreendente porque existem várias maneiras de iterar. Pense nisso: você poderia iterar por meio de um contêiner com uma etapa, um contêiner com outra etapa; ou através de um contêiner até chegar ao fim, então comece a inserir enquanto vai até o final do outro contêiner; ou uma etapa do primeiro contêiner para cada vez que você passar completamente pelo outro contêiner e depois reiniciar; ou algum outro padrão; ou mais de dois contêineres por vez; etc ...

No entanto, se você quiser fazer sua própria função de estilo "for_each" que itera por meio de dois contêineres apenas até o comprimento do mais curto, poderá fazer algo assim:

template <typename Container1, typename Container2>
void custom_for_each(
  Container1 &c1,
  Container2 &c2,
  std::function<void(Container1::iterator &it1, Container2::iterator &it2)> f)
{
  Container1::iterator begin1 = c1.begin();
  Container2::iterator begin2 = c2.begin();
  Container1::iterator end1 = c1.end();
  Container2::iterator end2 = c2.end();
  Container1::iterator i1;
  Container1::iterator i2;
  for (i1 = begin1, i2 = begin2; (i1 != end1) && (i2 != end2); ++it1, ++i2) {
    f(i1, i2);
  }
}

Obviamente, você pode fazer qualquer tipo de estratégia de iterações que desejar de maneira semelhante.

Claro, você pode argumentar que apenas fazer o loop for interno diretamente é mais fácil do que escrever uma função customizada como esta ... e você está certo, se for fazer isso apenas uma ou duas vezes. Mas o bom é que isso é muito reutilizável. =)

wjl
fonte
Parece que você tem que declarar os iteradores antes do loop? Eu tentei isso: for (Container1::iterator i1 = c1.begin(), Container2::iterator i2 = c2.begin(); (i1 != end1) && (i2 != end2); ++it1, ++i2)mas o compilador grita. Alguém pode explicar por que isso é inválido?
David Doria
@DavidDoria A primeira parte do loop for é uma única instrução. Você não pode declarar duas variáveis ​​de tipos diferentes na mesma instrução. Pense por que for (int x = 0, y = 0; ...funciona, mas for (int x = 0, double y = 0; ...)não funciona.
wjl
1
.. você pode, entretanto, ter std :: pair <Container1 :: iterator, Container2 :: iterator> its = {c1.begin (), c2.begin ()};
Lorro
1
Outra coisa a notar é que isso poderia ser facilmente tornado variável com C ++ 14'stypename...
wjl
8

No caso de você precisar iterar simultaneamente em apenas 2 contêineres, há uma versão estendida do algoritmo for_each padrão na biblioteca de intervalo de impulso, por exemplo:

#include <vector>
#include <boost/assign/list_of.hpp>
#include <boost/bind.hpp>
#include <boost/range/algorithm_ext/for_each.hpp>

void foo(int a, int& b)
{
    b = a + 1;
}

int main()
{
    std::vector<int> contA = boost::assign::list_of(4)(3)(5)(2);
    std::vector<int> contB(contA.size(), 0);

    boost::for_each(contA, contB, boost::bind(&foo, _1, _2));
    // contB will be now 5,4,6,3
    //...
    return 0;
}

Quando você precisa lidar com mais de 2 contêineres em um algoritmo, precisa brincar com o zip.

czarles
fonte
Maravilhoso! Como você encontrou? Parece que não está documentado em lugar nenhum.
Mikhail
4

outra solução poderia ser capturar uma referência do iterador do outro contêiner em um lambda e usar o operador de pós-incremento nisso. por exemplo, uma cópia simples seria:

vector<double> a{1, 2, 3};
vector<double> b(3);

auto ita = a.begin();
for_each(b.begin(), b.end(), [&ita](auto &itb) { itb = *ita++; })

dentro do lambda, você pode fazer qualquer coisa itae incrementá-lo. Isso se estende facilmente ao caso de vários contêineres.

Vahid
fonte
3

Uma biblioteca de alcance fornece esta e outras funcionalidades muito úteis. O exemplo a seguir usa Boost.Range . O rangev3 de Eric Niebler deve ser uma boa alternativa.

#include <boost/range/combine.hpp>
#include <iostream>
#include <vector>
#include <list>

int main(int, const char*[])
{
    std::vector<int> const v{0,1,2,3,4};
    std::list<char> const  l{'a', 'b', 'c', 'd', 'e'};

    for(auto const& i: boost::combine(v, l))
    {
        int ti;
        char tc;
        boost::tie(ti,tc) = i;
        std::cout << '(' << ti << ',' << tc << ')' << '\n';
    }

    return 0;
}

C ++ 17 tornará isso ainda melhor com ligações estruturadas:

int main(int, const char*[])
{
    std::vector<int> const v{0,1,2,3,4};
    std::list<char> const  l{'a', 'b', 'c', 'd', 'e'};

    for(auto const& [ti, tc]: boost::combine(v, l))
    {
        std::cout << '(' << ti << ',' << tc << ')' << '\n';
    }

    return 0;
}
Jens
fonte
Este programa não está compilando com g ++ 4.8.0. delme.cxx:15:25: error: no match for 'operator=' (operand types are 'std::tuple<int&, char&>' and 'const boost::tuples::cons<const int&, boost::tuples::cons<const char&, boost::tuples::null_type> >') std::tie(ti,tc) = i; ^
syam
Depois de alterar std :: tie para boost: tie, ele compilou.
syam
Recebo o seguinte erro de compilação para a versão com vinculação estruturada (usando a versão MSVC 19.13.26132.0e Windows SDK 10.0.16299.0): error C2679: binary '<<': no operator found which takes a right-hand operand of type 'const boost::tuples::cons<const char &,boost::fusion::detail::build_tuple_cons<boost::fusion::single_view_iterator<Sequence,boost::mpl::int_<1>>,Last,true>::type>' (or there is no acceptable conversion)
pooya13
vinculações estruturadas não parecem funcionar com boost::combine: stackoverflow.com/q/55585723/8414561
Dev Null
2

Também estou um pouco atrasado; mas você pode usar isto (função variadic de estilo C):

template<typename T>
void foreach(std::function<void(T)> callback, int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        std::vector<T> v = va_arg(args, std::vector<T>);
        std::for_each(v.begin(), v.end(), callback);
    }

    va_end(args);
}

foreach<int>([](const int &i) {
    // do something here
}, 6, vecA, vecB, vecC, vecD, vecE, vecF);

ou isto (usando um pacote de parâmetros de função):

template<typename Func, typename T>
void foreach(Func callback, std::vector<T> &v) {
    std::for_each(v.begin(), v.end(), callback);
}

template<typename Func, typename T, typename... Args>
void foreach(Func callback, std::vector<T> &v, Args... args) {
    std::for_each(v.begin(), v.end(), callback);
    return foreach(callback, args...);
}

foreach([](const int &i){
    // do something here
}, vecA, vecB, vecC, vecD, vecE, vecF);

ou isto (usando uma lista de inicializadores entre chaves):

template<typename Func, typename T>
void foreach(Func callback, std::initializer_list<std::vector<T>> list) {
    for (auto &vec : list) {
        std::for_each(vec.begin(), vec.end(), callback);
    }
}

foreach([](const int &i){
    // do something here
}, {vecA, vecB, vecC, vecD, vecE, vecF});

ou você pode juntar vetores como aqui: Qual é a melhor maneira de concatenar dois vetores? e, em seguida, itere sobre um grande vetor.

Szymon Marczak
fonte
0

Aqui está uma variante

template<class ... Iterator>
void increment_dummy(Iterator ... i)
    {}

template<class Function,class ... Iterator>
void for_each_combined(size_t N,Function&& fun,Iterator... iter)
    {
    while(N!=0)
        {
        fun(*iter...);
        increment_dummy(++iter...);
        --N;
        }
    }

Exemplo de uso

void arrays_mix(size_t N,const float* x,const float* y,float* z)
    {
    for_each_combined(N,[](float x,float y,float& z){z=x+y;},x,y,z);    
    }
user877329
fonte