patterncppMinor
Chaining function for range-based for loop
Viewed 0 times
rangeloopfunctionchainingforbased
Problem
Motivation: I'm relatively new to C++. I'm writing a function meant to be used with range-based for loops. The goal is to be able to iterate over multiple containers at once, each in turn (essentially Python's
Usage:
This should print out:
Implementation:
Working example also available at ideone.
```
#include
#include
#include
template
struct ref_or_copy
{
ref_or_copy(T&& rval)
: storage(new T(std::move(rval)))
, val(*storage) { }
ref_or_copy(T& lval) : val(lval) { }
std::unique_ptr storage;
T& val;
};
template
struct chain_obj_struct {
ref_or_copy> c1;
ref_or_copy> c2;
struct iterator {
decltype(std::begin(c1.val)) it1;
decltype(std::begin(c1.val)) it1_end;
decltype(std::begin(c2.val)) it2;
bool in_first;
iterator& operator++() {
if (in_first) {
++it1;
if (it1 == it1_end) {
in_first = false;
}
} else {
++it2;
}
return *this;
}
typename std::conditional>::value,
decltype(it1), decltype(it2)>::type
operator*()
{
if (in_first) return *it1;
return *it2;
}
bool operator==(const iterator& i2) {
if (in_first != i2.in_first) return false;
if (in_first)
return it1 == i2.it1;
else
return it2 == i2.it2;
}
bool operator!=(const iterator& i2) {
return !this->operator==(i2);
}
};
iterator begin() {
return iterator{std::begin(c1.val), std::end(c1.val), std::begin(c2.val), true};
}
iterator end() {
return iterator{std::end(c1.val), std::end(c1.val), std::end(c2.val), false};
}
};
template
chain_obj_struct chain_obj(C1&& c1, C2&& c2) {
retu
itertools.chain), with as littler overhead as possible (e.g. no copying containers).Usage:
vector a = {1, 2, 3, 4};
vector b = {5, 6, 7, 8};
list c = {9, 9, 4};
for (auto& i : chain_it(a, b, a, b, c)) {
cout << i << " ";
}This should print out:
1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 9 9 4Implementation:
Working example also available at ideone.
```
#include
#include
#include
template
struct ref_or_copy
{
ref_or_copy(T&& rval)
: storage(new T(std::move(rval)))
, val(*storage) { }
ref_or_copy(T& lval) : val(lval) { }
std::unique_ptr storage;
T& val;
};
template
struct chain_obj_struct {
ref_or_copy> c1;
ref_or_copy> c2;
struct iterator {
decltype(std::begin(c1.val)) it1;
decltype(std::begin(c1.val)) it1_end;
decltype(std::begin(c2.val)) it2;
bool in_first;
iterator& operator++() {
if (in_first) {
++it1;
if (it1 == it1_end) {
in_first = false;
}
} else {
++it2;
}
return *this;
}
typename std::conditional>::value,
decltype(it1), decltype(it2)>::type
operator*()
{
if (in_first) return *it1;
return *it2;
}
bool operator==(const iterator& i2) {
if (in_first != i2.in_first) return false;
if (in_first)
return it1 == i2.it1;
else
return it2 == i2.it2;
}
bool operator!=(const iterator& i2) {
return !this->operator==(i2);
}
};
iterator begin() {
return iterator{std::begin(c1.val), std::end(c1.val), std::begin(c2.val), true};
}
iterator end() {
return iterator{std::end(c1.val), std::end(c1.val), std::end(c2.val), false};
}
};
template
chain_obj_struct chain_obj(C1&& c1, C2&& c2) {
retu
Solution
Starting from the top.
Firstly, it's an odd name, since we're never copying. We're either referring or moving. But secondly, it's bigger than it needs to be, and incurs allocation overhead. We can hugely simplify this changing:
into:
Wait what? Consider the usage like:
In the lvalue case,
What about non-member begin?
This is definitely the Forgot About Dre of C++11 container programming:
Where you use
Chain of one
If try to do:
I would be quite surprised when
Since the last container always gets
For the
ref_or_copyFirstly, it's an odd name, since we're never copying. We're either referring or moving. But secondly, it's bigger than it needs to be, and incurs allocation overhead. We can hugely simplify this changing:
template
struct ref_or_copy
{ ... };
ref_or_copy> c1;
ref_or_copy> c2;into:
C1 c1;
C2 c2;Wait what? Consider the usage like:
template
T wrapped(T&& val) { return T{std::forward(val)}; }In the lvalue case,
T will be some type U&, so we're referring to the passed in val. In the rvalue case, T will be some type V, so we'll be move-constructed from val. Everything checks out.What about non-member begin?
This is definitely the Forgot About Dre of C++11 container programming:
struct Foo {
int arr[2]{10, 11};
};
int* begin(Foo& f) { return f.arr; }
int* end(Foo& f) { return f.arr + 2; }
Foo d;
for (auto i : chain_it(d, d)) { // error: no matching function call to 'begin'
}Where you use
std::begin and std::end, you'll have to make unqualified calls with the std:: functions in scope in order to do the right thing.Chain of one
If try to do:
std::vector a = {1, 2, 3, 4};
for (int& i : chain_it(a)) { ++i; }I would be quite surprised when
a[0] was still 1 at the end. I'd also be surprised that this doesn't compile:int d[] = {10, 11};
for (int& i : chain_it(d)) { ... }Since the last container always gets
chain_it()-ed as one, this is a serious issue. Both have the same root cause. The overload of chain_it that takes a single argument has the wrong return type:template
auto chain_it(C&& c) { return std::forward(c); }For the
vector, we're copying it. And for the array, we're decaying it and ending up with a pointer. We want to return an lvalue reference for lvalue references, and a value for rvalue references. That is, trivially:template
C chain_it(C&& c) { return std::forward(c); }Code Snippets
template <typename T>
struct ref_or_copy
{ ... };
ref_or_copy<std::remove_reference_t<C1>> c1;
ref_or_copy<std::remove_reference_t<C2>> c2;C1 c1;
C2 c2;template <typename T>
T wrapped(T&& val) { return T{std::forward<T>(val)}; }struct Foo {
int arr[2]{10, 11};
};
int* begin(Foo& f) { return f.arr; }
int* end(Foo& f) { return f.arr + 2; }
Foo d;
for (auto i : chain_it(d, d)) { // error: no matching function call to 'begin'
}std::vector<int> a = {1, 2, 3, 4};
for (int& i : chain_it(a)) { ++i; }Context
StackExchange Code Review Q#98680, answer score: 3
Revisions (0)
No revisions yet.