5

I often pack some data into class to prevent mistakes with public/global access and to provide some common methods on it, e.g.:

class GameArea{
    std::vector<Enemy*> enemies;
    std::wstring name;
public:
    void makeAllEnemiesScared();
    Enemy * getEnemy(unsigned index);
};

GameArea is just an simplified example here. It'd a kind of custom container/menager with some specialized methods (but it's not only an container).

The ideal situation is when I know what operations will be executed on each Enemy at once and they occur in several places so I can declare them in GameArea directly (like makeAllEnemiesScared()).

For other cases I can go with:

for(unsigned i=0; i<gameArea->getEnemiesCount(); i++){
    Enemy * enemy = gameArea->getEnemy(i);
    ...
}

But it suffers from some disadvantages:

  • I cannot use C++11 clean&nice for(auto &Enemy : enemies) loop,
  • It's not efficient (so many calls to getEnemy(index)),
  • It's not a purpose for getEnemy(index) to iterate throw all elements - it's usefull in case when we want to select single or few of them, it also has check for index < enemies.size() inside - it's terrible to check it on each element in loop.

NOTE: I think about cases when I do something very special (not worth creating separated method in GameArea, but on each element of GameArea::enemies).

I thought about some GameArea::onEachEnemy(... function ...) method that takes a function (or maybe better an lambda?) as a parameter. Is that good solution?

Or maybe different approach should be used here? Like returning the std::vector from GameArea - which looks a bit "ugly" to me. I don't want user to think he can actually add or remove items to/from that vector directly.

PolGraphic
  • 3,233
  • 11
  • 51
  • 108
  • 1
    Your "onEachEnemy" suggestion sounds good to me. – molbdnilo Dec 15 '14 at 13:03
  • 1
    I'm assuming that all `getEnemy` does is to return `enemies[i]`? Then you can inline the function and the compiler will most likely optimize the call away. You can also avoid the repeated `getEnemiesCount` call by calling it before the loop and saving the result. You can also provide an iterator interface, or, like you wonder, create a "for each" function. But first of all you should probably profile your program to see if this really is the bottle-neck you think it is. – Some programmer dude Dec 15 '14 at 13:06
  • Oh, and remember that the vector `operator[]` function doesn't do any bounds-checking, it assumes the user of the vector will take care to not go out of bounds. If you're so worried about inefficiency then you should probably do the same, i.e. drop the bounds checking from `getEnemy`. But like I said in my previous comment, *profile* first, and make sure this really is as inefficient as you think it is. – Some programmer dude Dec 15 '14 at 13:08
  • @JoachimPileborg it does a bit more: `if(i>=enemies.size()){ return 0;} return enemies[i]`. But I did not think about compiler optimization and moving the `getEnemiesCount` before the loop, thanks :) – PolGraphic Dec 15 '14 at 13:09
  • Even so, inlining the function might still help. – Some programmer dude Dec 15 '14 at 13:12

3 Answers3

4

If you expose the vector itself, or at least iterators begin and end, you can use std::for_each

Then you would use this as

std::for_each(std::begin(enemies), std::end(enemies), foo);  

where foo is the function you want to call on each Enemy.

Again note you don't have to expose the vector itself, you could make methods in GameArea to get iterators so the call could be like

std::for_each(gameArea->GetEnemyBegin(), gameArea->GetEnemyEnd(), foo);  
Cory Kramer
  • 114,268
  • 16
  • 167
  • 218
  • That's quite a smart alternative for my `GameArea::onEachEnemy(... function ...)`, thank you :) I will wait a bit for other responds to give a chance to get more ideas, but I've already up-voted it. – PolGraphic Dec 15 '14 at 13:11
  • 1
    Note that if you [expose iterators in the right way](http://stackoverflow.com/questions/8164567/how-to-make-my-custom-type-to-work-with-range-based-for-loops) it will work with C++11 ranged-based for loops as well. – Chris Drew Dec 15 '14 at 14:16
3

A good solution which does not expose internals of your classes is, as you suggested, a function calling an action for each object:

class GameArea {
 ...
  template<typename Func> void ForEachEnemy(Func f)
  {
    std::for_each(enemies.begin(), enemies.end(), f);
  }
 ...

Then you can pass anything you want as ForEachEnemy argument - global function, boost::bind result etc.

Wojtek Surowka
  • 20,535
  • 4
  • 44
  • 51
  • I finally went with that option, but for all the readers - check out ALL the responds because the proposed solutions (exposing vector itself, exposing begin/end iterator, using `has a` instead of `is a` relation) are good and may suit better to your own situation :) – PolGraphic Dec 19 '14 at 12:37
1

You might want to consider the Iterator pattern. That way you can keep the encapsulation but still have something that is convenient to use.

You can either expose standard container iterators or if you want complete encapsulation, write the Iterator yourself. If you write the Iterator correctly you can even use C++11 range-based for loops:

#include <vector>
#include <string>
#include <iostream>

struct Enemy{ std::string name; };

class EnemyIterator {
  friend class GameArea;
private:
  std::vector<Enemy>::iterator iterator;
  EnemyIterator(std::vector<Enemy>::iterator it) : iterator(it){}
public:
  EnemyIterator& operator++() { ++iterator; return *this; }
  Enemy& operator*() { return *iterator; }
  friend bool operator!=(EnemyIterator lhs, EnemyIterator rhs) {
    return lhs.iterator != rhs.iterator;
  }
};

class GameArea{
  std::vector<Enemy> enemies;
public:   
  EnemyIterator begin() { return EnemyIterator(enemies.begin()); }
  EnemyIterator end() { return EnemyIterator(enemies.end()); } 
  void addEnemy(std::string name) {enemies.push_back(Enemy{name}); }
};

int main() {
    GameArea area;
    area.addEnemy("Kobold");
    area.addEnemy("Wraith");
    area.addEnemy("Ork");
    for (auto& enemy : area)
      std::cout << enemy.name << "\n";
}

In order for C++11 ranged-based for loops to work with GameArea you need to do one of:

  • define begin and end member functions
  • define begin and end non-member functions in the same namespace
  • specialize std::begin and std::end
Chris Drew
  • 14,926
  • 3
  • 34
  • 54