Search code examples
c++templatesoverloadingrvalue-reference

Partial template specialization based on lval/rval?


I'm writing an adaptor class to allow me to write this:

for (auto item : ButFirst(myvec)) { ... }

The following class definition works well when, as above, the argument is an lval:

template <typename Container>
class ButFirst {
  const Container& container_;
public:
 ButFirst(const Container& container) : container_(container) {}
  typename Container::const_iterator begin() { return ++(container_.begin()); }
  typename Container::const_iterator end() { return container_.end(); }
};

However, I want to also use it when the argument is an rval, as in:

for (auto item : ButFirst(get_vec(3)) { ... }

I would therefore write the following class definition:

template <typename Container>
class ButFirst {
   const Container container_;

public:
  ButFirst(Container&& container) : container_(std::move(container)) {}
...
};

How can I write one class definition that handles both? Or maybe one class and some partial template specializations?


Solution

  • template <typename Container>
    class ButFirst {
      Container container_;
    public:
      ButFirst(Container container) : container_(std::forward<Container>(container)) {}
      typename std::decay_t<Container>::const_iterator begin() { return std::next(container_.begin()); }
      typename std::decay_t<Container>::const_iterator end() { return container_.end(); }
    };
    

    then add deduction guide:

    template <typename Container>
    ButFirst(Container&&)->ButFirst<Container>;
    

    and ... poof.

    Despite appearances this only copies on rvalues.

    The forwarding reference in the ButFirst deduction guide deduces Container to be a reference type when you construct it with an lvalue, and Container as a value type when you pass it an rvalue. This is exactly what you want.

    And that isn't a coincidence; forwarding references work that way for a reason.

    live example.

    As an aside, my version of this is a bit different.

    I define a range_view type that can be constructed from a range-like, and stores 2 iterators.

    range_view methods like begin() are const; range_view is like a pointer, not like a value.

    It has methods:

    range_view except_front(std::size_t n=1)const;
    range_view except_back(std::size_t n=1)const;
    range_view only_front(std::size_t n=1)const;
    range_view only_back(std::size_t n=1)const;
    

    which clamp n based on how big the range is.

    Then:

    for(auto item: range_view(container).except_front())
    

    does what yours does.

    I consider checking for iterator counting errors and returning empty range in that case well worth the overhead for the reliability I get from it, as that math happens once per loop, not once per iteration.

    Also, range_view has ridiculously many other useful uses.