0

I'm trying to pass an object to boost::adaptors::transformed. However, this only seems to work if that object's class defines const versions of begin and end. This is not the case for me however, because iterating over an object of this class modifies internal state of the object itself. Here is a minimal example using a dummy class Vector that only exposes non the const versions begin and end or its _v member, what can I change to make this work?

#include <initializer_list>
#include <vector>

#include <boost/range/adaptors.hpp>

template<typename T>
class Vector
{
public:
  using value_type = typename std::vector<T>::value_type;
  using reference = typename std::vector<T>::reference;
  using iterator = typename std::vector<T>::iterator;

  Vector(std::initializer_list<T> init)
  : _v(init)
  {}

  iterator begin() { return _v.begin(); }
  iterator end() { return _v.end(); }

private:
  std::vector<T> _v;
};

int main()
{
  Vector<int> v{1, 2, 3, 4};

  auto t = [](int i){ return 2 * i; };

  auto range(v | boost::adaptors::transformed(t)); // does not compile
}
Peter
  • 2,919
  • 1
  • 16
  • 35
  • If you still d'like to continue with the wrong design (modifying collection during iterating over it, will bring to bugs for sure) Make you vector `mutable std::vector _v;` and add const version. In any case why do you think boost authors add limitation to use constant iterators only ? Strongly recommend you not to use functional style with C++ without copying data. 2. Use your Haskell or whatever. – Victor Gubin Nov 18 '20 at 16:10
  • PS. All you need is: `#include #include int main(int argc,const char **argv) { std::array src = {1, 2, 3, 4}; std::array dst; for(std::size_t i=0; i < src.size(); i++) { dst[i] = src[i] << 1; } return 0; }` – Victor Gubin Nov 18 '20 at 16:27
  • @VictorGubin You'd think OP knows how to do that. Perhaps you've seen earlier questions by OP that I haven't seen, but there was no reason to assume big ignorance from just this question. – sehe Nov 18 '20 at 19:37
  • @sehe - I don't think OP have any ignorance. I think he trying to use imperative programming language as functional programing language. And it brings to real over designing. More complicated transformations can be done with the same trivial loop. – Victor Gubin Nov 18 '20 at 21:51
  • P.S. My understanding of ranges conception - improve operations like find_if where lambda can be used to search a structure in a container by a field. Or transform vector of one objects to vector or another objects. IMHO transform objects in place using lambda is kind of design perversion. Maybe Ok for some fully functional PL, not sure it is Ok C++. In fact a madness logic, with the code nobody can read including author in a few month, and compiler cannot optimize. – Victor Gubin Nov 18 '20 at 22:06
  • @VictorGubin That's getting pretty off-topic, doesn't address the question either. I will politely disagree with almost all the points you claim, but agree that functional programming has its own perversions. Peace – sehe Nov 18 '20 at 23:46

1 Answers1

2

I'd say in general it's a code smell that iteration modifies a collection.

Of course, something can be logically const, which is what we have the mutable keyword for. There's roughly two approaches I can see

Keep in mind that threading-aware libraries might assume that constoperations are threadsafety guarantees (so either bit-wise immutable OR e.g. only operation on synchronization primitives like a member mutex).

Make The Container Storage Mutable

Live On Compiler Explorer

#include <initializer_list>
#include <vector>
#include <boost/range/adaptors.hpp>
#include <fmt/ranges.h>

using boost::adaptors::transformed;

template<typename T>
class Vector {
    using Cont = std::vector<T>;
  public:
    using value_type     = typename Cont::value_type;
    using reference      = typename Cont::reference;
    using iterator       = typename Cont::iterator;
    using const_iterator = typename Cont::const_iterator;

    Vector(std::initializer_list<T> init) : _v(init) {}

    iterator       begin()          { return _v.begin(); } 
    iterator       end()            { return _v.end();   } 
    const_iterator begin() const    { return _v.begin(); } 
    const_iterator end() const      { return _v.end();   } 
    //const_iterator cbegin() const { return _v.begin(); } 
    //const_iterator cend() const   { return _v.end();   } 
  private:
    Cont mutable _v;
};

static auto twice(int i) { return 2 * i; }

int main() {
    fmt::print("{} -> {}\n",
        Vector {1, 2, 3, 4},
        Vector {1, 2, 3, 4} | transformed(twice));
}

Prints

{1, 2, 3, 4} -> {2, 4, 6, 8}

Purer Approach: Mutable Element Data

For fun, let's make an Element that tracks the number of times its value was observed:

Live On Compiler Explorer

#include <initializer_list>
#include <vector>
#include <boost/range/adaptors.hpp>
#include <fmt/ranges.h>

using boost::adaptors::transformed;

struct Element {
    Element(int value) : value(value) {}
    operator int() const { ++usage_counter; return value; }
    long usages() const { return usage_counter; }

  private:
    mutable long usage_counter = 0;
    int value;
};

template<typename T>
class Vector {
    using Cont = std::vector<T>;
  public:
    using value_type     = typename Cont::value_type;
    using reference      = typename Cont::reference;
    using iterator       = typename Cont::iterator;
    using const_iterator = typename Cont::const_iterator;

    Vector(std::initializer_list<T> init) : _v(init) {}

    iterator       begin()          { return _v.begin(); } 
    iterator       end()            { return _v.end();   } 
    const_iterator begin() const    { return _v.begin(); } 
    const_iterator end() const      { return _v.end();   } 
    //const_iterator cbegin() const { return _v.begin(); } 
    //const_iterator cend() const   { return _v.end();   } 
  private:
    Cont _v;
};

static auto twice(int i) { return 2 * i; }

int main() {
    Vector<Element> const v {1, 2, 3, 4}; // note const

    fmt::print("{} -> {} (usages {})\n",
        v,
        v | transformed(twice),
        v | transformed(std::mem_fn(&Element::usages))
    );
}

Prints

{1, 2, 3, 4} -> {2, 4, 6, 8} (usages {3, 3, 3, 3})
sehe
  • 374,641
  • 47
  • 450
  • 633
  • 1
    Huh, yeah I guess mutable solves this. I agree that the problem itself is weird, I didn't author the collection in question, maybe it would make more sense to move its relevant state into a separate object that every iterator holds a shared_ptr to or something. – Peter Nov 19 '20 at 18:03