Closed cleeff closed 4 years ago
Great question.
The short answer is that if you pass an lvalue to enumerate
then the Container
type in Enumerable
is an lvalue reference. So in this example Container
is std::vector<int>&
. The std::forward<Container>(container)
call thus only moves rvalues, no copies of the lvalue.
The long answer: there's a lot of layers of templates in the real cppitertools code, but here is a simpler example that demonstrates the technique:
#include <iostream>
namespace {
// a class that announces all its creation, copy, and destruction operators
class Talker {
public:
Talker() {
std::cout << "ctor\n";
}
Talker(const Talker&) {
std::cout << "copy ctor\n";
}
Talker& operator=(const Talker&) {
std::cout << "copy assign\n";
return *this;
}
~Talker() {
std::cout << "dtor\n";
}
};
template <typename T>
class Holder {
private:
T t_;
public:
Holder(T t) : t_{std::forward<T>(t)} {}
};
// if arg is an lvalue, T is an lvalue reference
// and Holder<T> has an lvalue reference as well
template <typename T>
Holder<T> make_holder(T&& arg) {
return {std::forward<T>(arg)};
}
}
int main() {
Talker talker;
auto h = make_holder(talker); // T will be Talker&
}
I'm not sure how much you know about forwarding references, but specifying T&&
as make_holder
's parameter's type means that if you pass an lvalue, the type T
resolves to Talker&
, so now you effectively have Talker& &&
as the type for arg
which by reference collapsing rules turns into Talker&
. The return type is then Holder<Talker&>
, so the Holder
template gets instantiated as
class Holder {
private:
Talker& t_;
public:
Holder(Talker& t) : t_{std::forward<Talker&>(t)} {}
};
so there's no copies. If you change the main to instead do make_holder(T{});
you will see copy constructor calls in the output. live example
The call to enumerate
is doing this (through a call operator in one of the template template bases here), and all the tools that take sequences follow this pattern in all sensible cases.
Does that make sense?
Wow, thanks a lot for the super detailed explanation. Learned something new about C++ :-)
The example for enumerate looks like this:
The documentation says that no copy of
vec
is produced here and I'm wondering how this works. In the implementation ofiter::impl::Enumerable
it looks like the class holds the container by value: