17

I want to initialize a container with pointers to objects. I currently have a loop like this:

for(int i=0;i < n;i++) {
    container.push_back(new Object());
}

Which C++ operation (i.e. similar to std::transform) is the right to replace this loop and initialize a container with n newly created objects?

allo
  • 3,955
  • 8
  • 40
  • 71
  • 2
    Are you asking for `std::generate_n(std::back_inserter(container), n, /*my lambda*/)`? – KABoissonneault Jul 12 '18 at 13:41
  • 1
    Better to use smart pointers instead of raw owning pointers. – Jarod42 Jul 12 '18 at 13:41
  • 1
    @Jarod42 To be fair, we don't know for sure that `container` doesn't contain smart pointers. – François Andrieux Jul 12 '18 at 13:43
  • @FrançoisAndrieux smart pointers with implicit constructors? Burn them all! – Quentin Jul 12 '18 at 13:46
  • @FrançoisAndrieux: a smart pointer accepting non explicit conversion... ouch (`emplace_back` might have been different, but then, it would leak if exception happens with container "resizing"). – Jarod42 Jul 12 '18 at 13:46
  • The API I am using gets raw pointers (and then takes ownership), so that's not my decision. – allo Jul 12 '18 at 13:51
  • @allo API gets the pointers via the container created? If not, I'd drop it entirely and just pass the objects to API directly (then in a loop again...). – Aconcagua Jul 12 '18 at 14:30
  • The API gets the pointers as single parameter, but I want to modify the objects later. The whole thing isn't something which needs to be optimized, but I wanted to learn a nice C++ idiom instead of a boring loop ;). – allo Jul 12 '18 at 14:38

3 Answers3

18

Use std::generate:

constexpr int n = 10;
std::vector<Object*> v1(n);
std::generate(v1.begin(), v1.end(), [](){ return new Object(); });

or std::generate_n:

std::vector<Object*> v2;
v2.reserve(n); // pre-allocate sufficient memory to prevent re-allocations
               // (you should have done in original loop approach as well)
std::generate_n(std::back_inserter(v2), n, [] { return new Object(); });
Aconcagua
  • 24,880
  • 4
  • 34
  • 59
Ron
  • 14,674
  • 4
  • 34
  • 47
  • I think a short example would greatly improve this answer. – François Andrieux Jul 12 '18 at 13:41
  • 1
    Note that in order to "push back" elements, you need `std::back_inserter` – KABoissonneault Jul 12 '18 at 13:41
  • I think I will use ``std::generate`` as it is cheap to initialize the container with the right capacity before using it and then it does not need to be resized while filling it. – allo Jul 12 '18 at 13:55
  • @allo: Even cheaper: `std::vector v; v.reserve(n); std::gen_n(...);` (reserve allocates a junk of memory, but doesn't initialise it - in contrast to `resize` or the constructor solution). – Aconcagua Jul 12 '18 at 13:56
  • @allo It's not cheap; it'll construct the container with n default-constructed elements and then assign them later. – songyuanyao Jul 12 '18 at 13:57
  • @songyuanyao Which is quite cheap for pointers ;). But I see, the reserve option seems to be even better. – allo Jul 12 '18 at 14:03
  • @Ron You shold add `std::vectorv;` to second approach to make clear that you need to start with *empty* vector (in contrast to first approach). Maybe add a hint to `reserve` as well? – Aconcagua Jul 12 '18 at 14:04
  • @Aconcagua Indeed. I even wonder if OP's use case could have been solved using a simple vector ctor overload: `std::vector v(n, new T());`? – Ron Jul 12 '18 at 14:12
  • 1
    @Ron`v(n, new T())` would have inserted the same pointer to all vector elements, which disqualified std::fill(_n) already... – Aconcagua Jul 12 '18 at 14:14
  • 1
    @Ron I wouldn't... OP didn't want to pick generate_n because he feared effiency loss (see comments above), but together with reserve, the approach actually is even superior... – Aconcagua Jul 12 '18 at 14:22
7

You could use std::generate_n and std::back_inserter with lambda.

std::generate_n(std::back_inserter(container), n, [] { return new Object(); });
songyuanyao
  • 169,198
  • 16
  • 310
  • 405
1

The goal is this syntax:

std::vector<Object*> v1 = generate([](auto&&){ return new Object; }, 10).make_container();

where we say we want to generate 10 elements with a specific lambda, then we create a container of the asked for type.

It requires some boilerplate. First, an input iterator that counts and calls a function:

template<class F>
struct generator_iterator {
  F f;
  std::size_t i = 0;

  using self=generator_iterator;
  friend bool operator==(self const& lhs, self const& rhs){ return lhs.i==rhs.i; }
  friend bool operator!=(self const& lhs, self const& rhs){ return lhs.i!=rhs.i; }
  using reference=std::result_of_t<F const&(std::size_t const&)>;
  using value_type=std::decay_t<reference>;
  using difference_type=std::ptrdiff_t;
  using pointer=value_type*;
  using iterator_category=std::input_iterator_tag;

  self& operator++(){++i; return *this;}
  self operator++(int){auto tmp=*this; ++*this; return tmp;}
  reference operator*()const{ return f(i); }
  pointer operator->()const { return std::addressof(f(i)); }

  friend difference_type operator-( self const& lhs, self const& rhs ) { return lhs.i-rhs.i; }
  self& operator-=( difference_type rhs )& {
    i-=rhs;
    return *this;
  }
  self& operator+=( difference_type rhs )& {
    i+=rhs;
    return *this;
  }
  friend difference_type operator+( self lhs, difference_type rhs ) {
    lhs += rhs;
    return lhs;
  }
  friend difference_type operator-( self lhs, difference_type rhs ) {
    lhs -= rhs;
    return lhs;
  }

};

Next, a range primitive, with a .make_container() method that lets you convert ranges to containers either by passing the type explicitly or implicitly:

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }

private:
  struct container_maker {
    range_t const* self;
    template<class C>
    operator C()&& {
      return {self->begin(), self->end()};
    }
  };
public:
  container_maker make_container()const{
    return {this};
  }
  // C is optional
  template<class C>
  C make_container()const{
    return make_container();
  }
};
template<class It>
range_t<It> range( It s, It f ) {
  return {std::move(s), std::move(f)};
}

We then glue these together:

template<class F>
auto generate( F&& f, std::size_t count ) {
  generator_iterator<std::decay_t<F>> e{f, count};
  generator_iterator<std::decay_t<F>> b{std::forward<F>(f)};
  return range( std::move(b), std::move(e) );
}

and this compiles:

std::vector<Object*> v1 = generate([](auto&&){ return new Object; }, 10).make_container();

Live example.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524