Cách tốt nhất để lặp lại đồng thời hai hoặc nhiều vùng chứa là gì


114

C ++ 11 cung cấp nhiều cách để lặp qua các vùng chứa. Ví dụ:

Vòng lặp dựa trên phạm vi

for(auto c : container) fun(c)

std :: for_each

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

Tuy nhiên, đâu là cách được khuyến nghị để lặp qua hai (hoặc nhiều) vùng chứa có cùng kích thước để thực hiện một số việc như:

for(unsigned i = 0; i < containerA.size(); ++i) {
  containerA[i] = containerB[i];
}

1
những gì về transformhiện tại #include <algorithm>?
Ankit Acharya

Giới thiệu về vòng lặp gán: nếu cả hai đều là vectơ hoặc giống nhau, hãy sử dụng containerA = containerB;thay cho vòng lặp.
emlai

Một câu hỏi tương tự: stackoverflow.com/questions/8511035/…
knedlsepp

1
Có thể có bản sao của hàm Sequence-zip cho c ++ 11?
underscore_d

Câu trả lời:


53

Đến bữa tiệc muộn hơn. Nhưng: Tôi sẽ lặp lại các chỉ số. Nhưng không phải với forvòng lặp cổ điển mà thay vào đó là forvòng lặp dựa trên phạm vi trên các chỉ số:

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

indiceslà một hàm trình bao bọc đơn giản trả về một phạm vi (được đánh giá một cách lười biếng) cho các chỉ số. Vì cách triển khai - mặc dù đơn giản - hơi quá dài để đăng nó ở đây, bạn có thể tìm thấy một cách triển khai trên GitHub .

Mã này hiệu quả như sử dụng forvòng lặp thủ công, cổ điển .

Nếu mẫu này thường xuyên xuất hiện trong dữ liệu của bạn, hãy xem xét sử dụng một mẫu khác zipgồm hai chuỗi và tạo ra một loạt các bộ giá trị, tương ứng với các phần tử được ghép nối:

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

Việc thực hiện zipđược để lại như một bài tập cho người đọc, nhưng nó dễ dàng theo dõi từ việc thực hiện indices.

(Trước C ++ 17, thay vào đó bạn phải viết như sau :)

for (auto items&& : zip(containerA, containerB))
    get<0>(items) = get<1>(items);

2
Có lợi thế nào của việc triển khai chỉ số của bạn so với việc tăng count_range không? Người ta có thể đơn giản sử dụngboost::counting_range(size_t(0), containerA.size())
SebastianK

3
@SebastianK Sự khác biệt lớn nhất trong trường hợp này là cú pháp: của tôi là (tôi khẳng định) khách quan tốt hơn để sử dụng trong trường hợp này. Hơn nữa, bạn có thể chỉ định kích thước bước. Xem trang Github được liên kết và cụ thể là tệp README để biết các ví dụ.
Konrad Rudolph

Ý tưởng của bạn rất hay và tôi đã nghĩ ra việc sử dụng count_range chỉ sau khi nhìn thấy nó: clear upvote :) Tuy nhiên, tôi tự hỏi liệu nó có cung cấp giá trị bổ sung để (lại) thực hiện điều này không. Ví dụ: liên quan đến hiệu suất. Tất nhiên, tôi đồng ý với cú pháp nhỏ hơn, nhưng chỉ cần viết một hàm tạo đơn giản để bù đắp nhược điểm này là đủ.
SebastianK

@SebastianK Tôi thừa nhận rằng khi viết mã, tôi cho rằng nó đủ đơn giản để sống biệt lập mà không cần sử dụng thư viện (và đúng như vậy!). Bây giờ tôi có lẽ sẽ viết nó như một trình bao bọc xung quanh Boost.Range. Điều đó nói lên rằng hiệu suất của thư viện của tôi đã tối ưu. Ý tôi muốn nói ở đây là việc sử dụng indicestriển khai của tôi mang lại kết quả đầu ra của trình biên dịch giống như sử dụng forcác vòng lặp thủ công. Không có bất kỳ chi phí nào.
Konrad Rudolph

Vì dù sao tôi cũng sử dụng boost nên trong trường hợp của tôi sẽ đơn giản hơn. Tôi đã viết trình bao bọc này xung quanh phạm vi tăng cường: một hàm với một dòng mã là tất cả những gì tôi cần. Tuy nhiên, tôi sẽ quan tâm nếu hiệu suất của phạm vi tăng là tối ưu.
SebastianK

38

Đối với ví dụ cụ thể của bạn, chỉ cần sử dụng

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

Đối với trường hợp tổng quát hơn, bạn có thể sử dụng Boost.Iterator's zip_iterator, với một chức năng nhỏ để làm cho nó có thể sử dụng được trong các vòng lặp dựa trên phạm vi. Đối với hầu hết các trường hợp, điều này sẽ hoạt động:

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";

Ví dụ trực tiếp.

Tuy nhiên, đối genericity toàn diện, có thể bạn muốn một cái gì đó giống như này , mà sẽ làm việc một cách chính xác đối với mảng và kiểu do người dùng định nghĩa mà không có thành viên begin()/ end()nhưng làmbegin/ endchức năng trong không gian tên của họ. Ngoài ra, điều này sẽ cho phép người dùng có constquyền truy cập cụ thể thông qua các zip_c...chức năng.

Và nếu bạn là người ủng hộ các thông báo lỗi đẹp, giống như tôi, thì bạn có thể muốn điều này , nó kiểm tra xem có bất kỳ vùng chứa tạm thời nào được chuyển cho bất kỳ zip_...hàm nào không và in ra một thông báo lỗi đẹp nếu có.


1
Cảm ơn! Tuy nhiên, một câu hỏi đặt ra, tại sao bạn sử dụng auto &&, nghĩa là gì &&?
memecs

@memecs: Tôi khuyên bạn nên đọc qua câu hỏi này , cũng như câu trả lời này của tôi , nó giải thích cách suy luận và thu gọn tham chiếu được thực hiện. Lưu ý rằng autohoạt động giống hệt như một tham số mẫu và T&&trong một mẫu là một tham chiếu chung như được giải thích trong liên kết đầu tiên, vì vậy auto&& v = 42sẽ được suy luận là int&&auto&& w = v;sau đó sẽ được suy ra là int&. Nó cho phép bạn so khớp các giá trị cũng như các giá trị và cho phép cả hai đều có thể thay đổi được mà không cần tạo bản sao.
Xeo

@Xeo: Nhưng ưu điểm của auto && so với auto & in a foreach loop là gì?
Viktor Sehr

@ViktorSehr: Nó cho phép bạn liên kết với các phần tử tạm thời, như những phần tử được tạo ra bởi zip_range.
Xeo

23
@Xeo Tất cả các liên kết đến các ví dụ đều bị hỏng.
kynan

34

Tôi tự hỏi tại sao không ai đề cập đến điều này:

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;
    }
}

Tái bút: nếu kích thước vùng chứa không khớp, thì bạn sẽ phải đặt mã bên trong câu lệnh if.


9

Có rất nhiều cách để thực hiện những việc cụ thể với nhiều vùng chứa như được cung cấp trong algorithmtiêu đề. Ví dụ, trong ví dụ bạn đã đưa ra, bạn có thể sử dụng std::copythay vì một vòng lặp for rõ ràng.

Mặt khác, không có bất kỳ cách tích hợp nào để lặp lại nhiều vùng chứa một cách chung chung ngoài vòng lặp for thông thường. Điều này không đáng ngạc nhiên vì có rất nhiều cách để lặp lại. Hãy nghĩ về nó: bạn có thể lặp lại một vùng chứa với một bước, một vùng chứa với một bước khác; hoặc qua một vùng chứa cho đến khi nó đến cuối rồi bắt đầu chèn trong khi bạn đi qua phần cuối của vùng chứa kia; hoặc một bước của vùng chứa đầu tiên cho mỗi lần bạn hoàn toàn đi qua vùng chứa khác rồi bắt đầu lại; hoặc một số mẫu khác; hoặc nhiều hơn hai thùng chứa cùng một lúc; Vân vân ...

Tuy nhiên, nếu bạn muốn tạo một hàm kiểu "for_each" của riêng mình để chỉ lặp qua hai vùng chứa có độ dài bằng chiều dài của vùng ngắn nhất, bạn có thể làm như sau:

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);
  }
}

Rõ ràng là bạn có thể thực hiện bất kỳ loại chiến lược lặp lại nào bạn muốn theo cách tương tự.

Tất nhiên, bạn có thể tranh luận rằng chỉ thực hiện trực tiếp vòng lặp for bên trong sẽ dễ dàng hơn là viết một hàm tùy chỉnh như thế này ... và bạn đã đúng, nếu bạn chỉ làm điều đó một hoặc hai lần. Nhưng điều tốt đẹp là nó rất có thể tái sử dụng. =)


Có vẻ như bạn phải khai báo các vòng lặp trước vòng lặp? Tôi đã thử điều này: for (Container1::iterator i1 = c1.begin(), Container2::iterator i2 = c2.begin(); (i1 != end1) && (i2 != end2); ++it1, ++i2)nhưng trình biên dịch kêu lên. Bất cứ ai có thể giải thích tại sao điều này là không hợp lệ?
David Doria

@DavidDoria Phần đầu tiên của vòng lặp for là một câu lệnh đơn. Bạn không thể khai báo hai biến có kiểu khác nhau trong cùng một câu lệnh. Hãy suy nghĩ về lý do tại sao for (int x = 0, y = 0; ...hoạt động, nhưng for (int x = 0, double y = 0; ...)không.
wjl

1
.. bạn có thể, tuy nhiên, có std :: pair <Container1 :: iterator, Container2 :: iterator> its = {c1.begin (), c2.begin ()};
lorro

1
Một điều cần lưu ý là điều này có thể dễ dàng làm variadic với C ++ 14 củatypename...
wjl

8

Trong trường hợp bạn chỉ cần lặp lại đồng thời trên 2 vùng chứa, có một phiên bản mở rộng của thuật toán for_each tiêu chuẩn trong thư viện phạm vi tăng cường, ví dụ:

#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;
}

Khi bạn cần xử lý nhiều hơn 2 vùng chứa trong một thuật toán, thì bạn cần chơi với zip.


Tuyệt vời! Làm thế nào bạn tìm được? Có vẻ như nó không được ghi lại ở bất cứ đâu.
Mikhail

4

một giải pháp khác có thể là nắm bắt một tham chiếu của trình vòng lặp của vùng chứa khác trong lambda và sử dụng toán tử tăng sau trên đó. ví dụ bản sao đơn giản sẽ là:

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++; })

bên trong lambda, bạn có thể làm bất cứ điều gì với itavà sau đó tăng nó lên. Điều này dễ dàng mở rộng cho nhiều trường hợp container.


3

Thư viện phạm vi cung cấp chức năng này và chức năng rất hữu ích khác. Ví dụ sau sử dụng Boost.Range . Eric Niebler's rangev3 sẽ là một lựa chọn tốt.

#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 sẽ làm cho điều này tốt hơn với các ràng buộc có cấu trúc:

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;
}

Chương trình này không biên dịch với 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

Sau khi thay đổi std :: tie thành boost: tie, nó đã được biên dịch.
syam

Tôi gặp lỗi biên dịch sau cho phiên bản có cấu trúc ràng buộc (sử dụng MSVC 19.13.26132.0và phiên bản 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

các ràng buộc có cấu trúc dường như không hoạt động với boost::combine: stackoverflow.com/q/55585723/8414561
Dev Null

2

Tôi cũng hơi muộn; nhưng bạn có thể sử dụng điều này (hàm biến thể kiểu 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);

hoặc cái này (sử dụng gói tham số hàm):

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);

hoặc cái này (sử dụng danh sách bộ khởi tạo có dấu ngoặc nhọn):

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});

hoặc bạn có thể nối các vectơ như ở đây: Cách tốt nhất để nối hai vectơ là gì? và sau đó lặp qua vector lớn.


0

Đây là một biến thể

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;
        }
    }

Ví dụ sử dụng

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);    
    }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.