c++boostboost-range

boost::adaptors::transformed for classes without const begin/end


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
}

Solution

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