1

I have a Storage class that keeps a list of Things:

#include <iostream>
#include <list>
#include <functional>

class Thing {
    private:
        int id;
        int value = 0;
        static int nextId;
    public:
        Thing() { this->id = Thing::nextId++; };
        int getId() const { return this->id; };
        int getValue() const { return this->value; };
        void add(int n) { this->value += n; };
};
int Thing::nextId = 1;

class Storage {
    private:
        std::list<std::reference_wrapper<Thing>> list;
    public:
        void add(Thing& thing) {
            this->list.push_back(thing);
        }
        Thing& findById(int id) const {
            for (std::list<std::reference_wrapper<Thing>>::const_iterator it = this->list.begin(); it != this->list.end(); ++it) {
                if (it->get().getId() == id) return *it;
            }
            std::cout << "Not found!!\n";
            exit(1);
        }
};

I started with a simple std::list<Thing>, but then everything is copied around on insertion and retrieval, and I didn't want this because if I get a copy, altering it does not reflect on the original objects anymore. When looking for a solution to that, I found about std::reference_wrapper on this SO question, but now I have another problem.

Now to the code that uses them:

void temp(Storage& storage) {
    storage.findById(2).add(1);
    Thing t4; t4.add(50);
    storage.add(t4);
    std::cout << storage.findById(4).getValue() << "\n";
}

void run() {
    Thing t1; t1.add(10);
    Thing t2; t2.add(100);
    Thing t3; t3.add(1000);

    Storage storage;
    storage.add(t3);
    storage.add(t1);
    storage.add(t2);

    temp(storage);

    t2.add(10000);

    std::cout << storage.findById(2).getValue() << "\n";
    std::cout << storage.findById(4).getValue() << "\n";
}

My main() simply calls run(). The output I get is:

50
10101
Not found!!

Although I was looking for:

50
10101
50

Question

Looks like the locally declared object t4 ceases to exist when the function returns, which makes sense. I could prevent this by dynamically allocating it, using new, but then I didn't want to manage memory manually...

How can I fix the code without removing the temp() function and without having to manage memory manually?

If I just use a std::list<Thing> as some suggested, surely the problem with t4 and temp will cease to exist, but another problem will arise: the code won't print 10101 anymore, for example. If I keep copying stuff around, I won't be able to alter the state of a stored object.

Pedro A
  • 3,989
  • 3
  • 32
  • 56
  • There are no pointers anywhere in this, so it's a little confusing. Did you mean references? –  Jun 01 '18 at 18:28
  • On a side note, this is another reason why Java is not a good first language and a lower lever C/C++ should be taught. – DeiDei Jun 01 '18 at 18:30
  • 1
    Also, your code contains Undefined Behavior, as `temp()` stores a `reference_wrapper` to a local variable in a container that lives past the end of the function. –  Jun 01 '18 at 18:30
  • Unrelated: read up on the [Member Initalizer List](http://en.cppreference.com/w/cpp/language/initializer_list). It will save you time and trouble later. – user4581301 Jun 01 '18 at 18:30
  • 1
    `t4` ceases to exist after you return from `temp`. Your reference refers to a destroyed object. – François Andrieux Jun 01 '18 at 18:30
  • @Frank no I meant pointers. Yes there are no pointers because I am trying to not use pointers :) But it's not perfectly working yet. – Pedro A Jun 01 '18 at 18:32
  • 1
    no pointers in your code and using a reference_wrapper just opens the door to errors and UB. Just use plain objects always, and then you will realize when you need a pointer/reference – 463035818_is_not_an_ai Jun 01 '18 at 18:32
  • Pointers are not necessarily evil. Read up on `std::unique_ptr` and `std::shared_ptr`. `std::unique_ptr` manages an object that it deletes when it goes out of scope. `std::shared_ptr` does reference counting. – Paul Sanders Jun 01 '18 at 18:32
  • The nearest analogue to how object handles work in Java may be `std::shared_ptr`. It's certainly not `T*`. You should not use raw pointers to represent ownership. When done properly, modern code does not need to remember to call `delete`. – François Andrieux Jun 01 '18 at 18:32
  • Okay so apparently the main issue is that `t4` ceases to exist when `temp` returns. So the only solution is really to use pointers here, calling `new` in `temp` so that it does not cease to exist? – Pedro A Jun 01 '18 at 18:33
  • 2
    @Hamsterrific Don't use `new` to create new objects. Modern code uses `std::make_unique` or `std::make_shared`. See https://stackoverflow.com/questions/106508/what-is-a-smart-pointer-and-when-should-i-use-one?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa – François Andrieux Jun 01 '18 at 18:34
  • 3
    "So the only solution is really to use pointers here" not really. Just make a `std::list` and let the list own the `Things` and the problem is gone – 463035818_is_not_an_ai Jun 01 '18 at 18:35
  • @FrançoisAndrieux thanks for the suggestions - should I really be learning that as a beginner though? Or is it really the only way to do this correctly in C++? – Pedro A Jun 01 '18 at 18:35
  • 2
    Do not confuse pointers and dynamic allocation. Think in terms of ownership. Who is responsible for the care and feeding and ultimately the freeing of an object. – user4581301 Jun 01 '18 at 18:36
  • @user463035818 that's not true. If I use `std::list` indeed that problem is gone but other problems arise, the code won't print 10101 anymore for example... – Pedro A Jun 01 '18 at 18:36
  • @Hamsterrific This is the correct way of creating objects on the heap. Though c++ *strongly* favors value semantics these days. user463035818's suggestion of using a simple `std::list` would be the preferred solution. – François Andrieux Jun 01 '18 at 18:36
  • Right now, it's not at all clear (at least to me) what all this accomplishes that `std::vector` won't do at least as well (and mostly better). If you were creating non-contiguous IDs, then you might want `std::map` instead, but right now your `id`s are contiguous, starting from 0, so they're equivalent to indices into a vector. – Jerry Coffin Jun 01 '18 at 18:37
  • if the code prints something wrong when you use a `list` then there is something else wrong. You really dont need pointers here and reference only to pass the list to that method – 463035818_is_not_an_ai Jun 01 '18 at 18:37
  • @user463035818 As the example is now, a `list` won't work since the original element is modified after insertion. – François Andrieux Jun 01 '18 at 18:38
  • @FrançoisAndrieux uh sorry didnt spot that, but honstely that is something that should be fixed as well – 463035818_is_not_an_ai Jun 01 '18 at 18:39
  • Thanks everyone so far, I've added an edit clarifying the problem. I started using just a `list` but I subsequent edits on the objects weren't reflecting in the stored things, since they were copied... This way I found std::reference_wrapper, now I have another issue. Recall that I am just a beginner in C++, if there is a better way without reference_wrapper, I would be happy to know as well :) – Pedro A Jun 01 '18 at 18:41
  • 2
    @Hamsterrific The idiomatic solution would be to store your `Thing` in `Storage` and then get and use a reference to that element, since `Storage` really owns the `Thing`s. As the design is now, it seems like `Storage` actually shares ownership. `Storage` necessarily needs to extend the lifetime of `Thing`s until it's own lifetime ends, but you also seem to want the `run` function to keep it local. In this case, a `std::list>` would serve your purpose. A `std::shared_ptr` can be copied and assigned and last copy will clean up the `Thing` when it's destroyed. – François Andrieux Jun 01 '18 at 18:43
  • See [What is a smart pointer and when should I use one?](https://stackoverflow.com/questions/106508/what-is-a-smart-pointer-and-when-should-i-use-one?). An [`std::shared_ptr`](http://en.cppreference.com/w/cpp/memory/shared_ptr) will offer you semantics you should be familiar with, coming from Java. – François Andrieux Jun 01 '18 at 18:46
  • Always prefer using containers and smart pointers over manual memory management. – Jesper Juhl Jun 01 '18 at 18:47
  • 2
    C++ is probably the worst language to learn by trial and error, due to the concept of undefined behavior. Sometimes you do really bad things, but program 'works' - i.e. produces expected result - but than magically stops working once you add a completely unrelated piece of code. Get yourself a good book and learn systematically. – SergeyA Jun 01 '18 at 18:59

5 Answers5

7

Who is the owner of the Thing in the Storage?

Your actual problem is ownership. Currently, your Storage does not really contain the Things but instead it is left to the user of the Storage to manage the lifetime of the objects you put inside it. This is very much against the philosophy of std containers. All standard C++ containers own the objects you put in them and the container manages their lifetime (eg you simply call v.resize(v.size()-2) on a vector and the last two elements get destroyed).

Why references?

You already found a way to make the container not own the actual objects (by using a reference_wrapper), but there is no reason to do so. Of a class called Storage I would expect it to hold objects not just references. Moreover, this opens the door to lots of nasty problems, including undefined behaviour. For example here:

void temp(Storage& storage) {
    storage.findById(2).add(1);
    Thing t4; t4.add(50);
    storage.add(t4);
    std::cout << storage.findById(4).getValue() << "\n";
}

you store a reference to t4 in the storage. The thing is: t4s lifetime is only till the end of that function and you end up with a dangling reference. You can store such a reference, but it isnt that usefull because you are basically not allowed to do anything with it.

Aren't references a cool thing?

Currently you can push t1, modify it, and then observe that changes on the thingy in Storage, this might be fine if you want to mimic Java, but in c++ we are used to containers making a copy when you push something (there are also methods to create the elements in place, in case you worry about some useless temporaries). And yes, of course, if you really want you can make a standard container also hold references, but lets make a small detour...

Who collects all that garbage?

Maybe it helps to consider that Java is garbage-collected while C++ has destructors. In Java you are used to references floating around till the garbage collector kicks in. In C++ you have to be super aware of the lifetime of your objects. This may sound bad, but acutally it turns out to be extremely usefull to have full control over the lifetime of objects.

Garbage? What garbage?

In modern C++ you shouldnt worry to forget a delete, but rather appreciate the advantages of having RAII. Acquiring resources on initialzation and knowing when a destructor gets called allows to get automatic resource management for basically any kind of resource, something a garbage collector can only dream of (think of files, database connections, etc.).

"How can I fix the code without removing the temp() function and without having to manage memory manually?"

A trick that helped me a lot is this: Whenever I find myself thinking I need to manage a resource manually I stop and ask "Can't someone else do the dirty stuff?". It is really extremely rare that I cannot find a standard container that does exactly what I need out of the box. In your case, just let the std::list do the "dirty" work.

Can't be C++ if there is no template, right?

I would actually suggest you to make Storage a template, along the line of:

template <typename T>
class Storage {
private:
    std::list<T> list;
//....

Then

Storage<Thing> thing_storage;
Storage<int> int_storage;

are Storages containing Things and ints, respectively. In that way, if you ever feel like exprimenting with references or pointers you could still instantiate a Storage<reference_wrapper<int>>.

Did I miss something?...maybe references?

I won't be able to alter the state of a stored object

Given that the container owns the object you would rather let the user take a reference to the object in the container. For example with a vector that would be

auto t = std::vector<int>(10,0);  // 10 element initialized to 0
auto& first_element = t[0];       // reference to first element
first_element = 5;                // first_element is an alias for t[0]
std::cout << t[0];                // i dont want to spoil the fun part

To make this work with your Storage you just have to make findById return a reference. As a demo:

struct foo {
    private: 
        int data;
    public:
        int& get_ref() { return data;}
        const int& get_ref() const { return data;}
};

auto x = foo();
x.get_ref = 12;

TL;DR

How to avoid manual resource managment? Let someone else do it for you and call it automatic resource management :P

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • 1
    If the OP wants to have some sort of shared ownership, could use a `std::list> list;`. – Eljay Jun 01 '18 at 18:58
  • @Eljay sure they could, however i didnt see any hint for that ;). Anyhow I would suggest to make `Storage` a template and let the user choose what they want – 463035818_is_not_an_ai Jun 01 '18 at 19:00
  • Thank you very much!! This is immensely helpful, +1. Indeed, I will have to rethink the ownership between my objects. When I asked the question I made up these `Thing` and `Storage` classes to create a minimal verifiable example, but in reality I have more classes and more relationships, so I can't say yet that I can just delegate the ownership control to a single class. (I'll do my best though, it's just that coming from Java I'm not used to thinking about these things). Regardless, I learned a lot from your answer :) – Pedro A Jun 01 '18 at 20:01
  • 1
    @Hamsterrific just in case I couldnt convince you, here is a video about the [downsides of garbage collection](https://www.youtube.com/watch?v=YihiSqO4jnA) ;) just joking, by no means it was meant as a rant against java, i just tried to highlight the main differences, glad if it helped – 463035818_is_not_an_ai Jun 01 '18 at 20:08
  • Hahaha absolutely. Thanks again! Great video :P – Pedro A Jun 01 '18 at 20:11
1

t4 is a temporary object that is destroyed at exit from temp() and what you store in storage becomes a dangling reference, causing UB.

It is not quite clear what you're trying to achieve, but if you want to keep the Storage class the same as it is, you should make sure that all the references stored into it are at least as long-lived as the storage itself. This you have discovered is one of the reasons STL containers keep their private copies of elements (others, probably less important, being—elimination of an extra indirection and a much better locality in some cases).

P.S. And please, can you stop writing those this-> and learn about initialization lists in constructors? >_<

bipll
  • 11,747
  • 1
  • 18
  • 32
  • 1
    Your "P.S." comes off as a bit rude and condescending, as though everyone should implicitly be aware of member initializer lists and implicit `this->`. It makes not allowance for those who haven't had the time to reach those parts of the language. It would be more helpful and respectful to instead explain these concepts or direct to sources that would explain them. – François Andrieux Jun 01 '18 at 19:01
  • 1
    I have to say, I agree with this comment. The OP is clearly doing his best, there's no need to make him feel small. – Paul Sanders Jun 02 '18 at 09:05
1

In terms of what your code actually appears to be doing, you've definitely overcomplicated your code, by my estimation. Consider this code, which does all the same things your code does, but with far less boilerplate code and in a way that's far more safe for your uses:

#include<map>
#include<iostream>

int main() {
    std::map<int, int> things;
    int & t1 = things[1];
    int & t2 = things[2];
    int & t3 = things[3];
    t1 = 10;
    t2 = 100;
    t3 = 1000;
    t2++;
    things[4] = 50;
    std::cout << things.at(4) << std::endl;
    t2 += 10000;
    std::cout << things.at(2) << std::endl;
    std::cout << things.at(4) << std::endl;
    things.at(2) -= 75;
    std::cout << things.at(2) << std::endl;
    std::cout << t2 << std::endl;
}

//Output:
50
10101
50
10026
10026

Note that a few interesting things are happening here:

  • Because t2 is a reference, and insertion into the map doesn't invalidate references, t2 can be modified, and those modifications will be reflected in the map itself, and vise-versa.
  • things owns all the values that were inserted into it, and it will be cleaned up due to RAII, and the built-in behavior of std::map, and the broader C++ design principles it is obeying. There's no worry about objects not being cleaned up.

If you need to preserve the behavior where the id incrementing is handled automatically, independently from the end-programmer, we could consider this code instead:

#include<map>
#include<iostream>

int & insert(std::map<int, int> & things, int value) {
    static int id = 1;
    int & ret = things[id++] = value;
    return ret;
}

int main() {
    std::map<int, int> things;
    int & t1 = insert(things, 10);
    int & t2 = insert(things, 100);
    int & t3 = insert(things, 1000);
    t2++;
    insert(things, 50);
    std::cout << things.at(4) << std::endl;
    t2 += 10000;
    std::cout << things.at(2) << std::endl;
    std::cout << things.at(4) << std::endl;
    things.at(2) -= 75;
    std::cout << things.at(2) << std::endl;
    std::cout << t2 << std::endl;
}

//Output:
50
10101
50
10026
10026

These code snippets should give you a decent sense of how the language works, and what principles, possibly unfamiliar in the code I've written, that you need to learn about. My general recommendation is to find a good C++ resource for learning the basics of the language, and learn from that. Some good resources can be found here.

One last thing: if the use of Thing is critical to your code, because you need more data saved in the map, consider this instead:

#include<map>
#include<iostream>
#include<string>

//Only difference between struct and class is struct sets everything public by default
struct Thing {
    int value;
    double rate;
    std::string name;
    Thing() : Thing(0,0,"") {}
    Thing(int value, double rate, std::string name) : value(value), rate(rate), name(std::move(name)) {}
};

int main() {
    std::map<int, Thing> things;
    Thing & t1 = things[1];
    t1.value = 10;
    t1.rate = 5.7;
    t1.name = "First Object";
    Thing & t2 = things[2];
    t2.value = 15;
    t2.rate = 17.99999;
    t2.name = "Second Object";

    t2.value++;
    std::cout << things.at(2).value << std::endl;
    t1.rate *= things.at(2).rate;
    std::cout << things.at(1).rate << std::endl;

    std::cout << t1.name << "," << things.at(2).name << std::endl;
    things.at(1).rate -= 17;
    std::cout << t1.rate << std::endl;
}
Xirema
  • 19,889
  • 4
  • 32
  • 68
  • 1
    Thank you, this is a great complement to the other answers. It is helpful, but removing the classes altogether broke the purpose a bit, since what I constructed was a minimal example to reflect my actual code, with much more classes and lines of code, and I don't think I can wipe all of them out in my real situation. Regardless, thanks and +1 from me :) – Pedro A Jun 01 '18 at 19:42
  • @Hamsterrific I've added a section at the end showing the use of a class within the map. – Xirema Jun 01 '18 at 19:50
  • Hamsterrific, I bet a `map` can get rid of some of those lines of code. – Paul Sanders Jun 01 '18 at 20:49
1

Based on what François Andrieux and Eljay have said (and what I would have said, had I got there first), here is the way I would do it, if you want to mutate objects you have already added to a list. All that reference_wrapper stuff is just a fancy way of passing pointers around. It will end in tears.

OK. here's the code (now edited as per OP's request):

#include <iostream>
#include <list>
#include <memory>

class Thing {
    private:
        int id;
        int value = 0;
        static int nextId;
    public:
        Thing() { this->id = Thing::nextId++; };
        int getId() const { return this->id; };
        int getValue() const { return this->value; };
        void add(int n) { this->value += n; };
};
int Thing::nextId = 1;

class Storage {
    private:
        std::list<std::shared_ptr<Thing>> list;
    public:
        void add(const std::shared_ptr<Thing>& thing) {
            this->list.push_back(thing);
        }
        std::shared_ptr<Thing> findById(int id) const {
            for (std::list<std::shared_ptr<Thing>>::const_iterator it = this->list.begin(); it != this->list.end(); ++it) {
                if (it->get()->getId() == id) return *it;
            }
            std::cout << "Not found!!\n";
            exit(1);
        }
};

void add_another(Storage& storage) {
    storage.findById(2)->add(1);
    std::shared_ptr<Thing> t4 = std::make_shared<Thing> (); t4->add(50);
    storage.add(t4);
    std::cout << storage.findById(4)->getValue() << "\n";
}

int main() {
    std::shared_ptr<Thing> t1 = std::make_shared<Thing> (); t1->add(10);
    std::shared_ptr<Thing> t2 = std::make_shared<Thing> (); t2->add(100);
    std::shared_ptr<Thing> t3 = std::make_shared<Thing> (); t3->add(1000);

    Storage storage;
    storage.add(t3);
    storage.add(t1);
    storage.add(t2);

    add_another(storage);

    t2->add(10000);

    std::cout << storage.findById(2)->getValue() << "\n";
    std::cout << storage.findById(4)->getValue() << "\n";
    return 0;
}

Output is now:

50
10101
50

as desired. Run it on Wandbox.

Note that what you are doing here, in effect, is reference counting your Things. The Things themselves are never copied and will go away when the last shared_ptr goes out of scope. Only the shared_ptrs are copied, and they are designed to be copied because that's their job. Doing things this way is almost as efficient as passing references (or wrapped references) around and far safer. When starting out, it's easy to forget that a reference is just a pointer in disguise.

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48
  • 1
    Thanks for this. This seems really nice, and a great complement to the other answers. Could you edit your answer to keep the `temp()` function with the intended behavior using those `shared_ptr`s? – Pedro A Jun 01 '18 at 19:39
  • You can do that - anyone can edit a post and then it gets peer-reviewed. Just explain in the comments box why you are doing it. I left it out because I didn't understand it. ... ... ... Do I get an upvote? :) – Paul Sanders Jun 01 '18 at 20:01
  • Oh, I know that I can propose edits :) But since I just heard about `shared_ptr`s, I don't know how to do it (the code), so that's why I was asking you to further expand your answer... – Pedro A Jun 01 '18 at 20:03
  • Well OK, although it's really just more of the same. I gave `temp` a more sensible name. – Paul Sanders Jun 01 '18 at 20:43
  • Ahh, I see, it's just the same thing. I thought perhaps there might be some tricks or complications. Thanks a lot!! :) – Pedro A Jun 01 '18 at 20:49
  • Yep. Some dots change to `->` because `shared_ptr` overloads `operator->` to let you get at the object it is managing. Other than that, and the calls to `make_shared`, your code is pretty much unaltered. – Paul Sanders Jun 01 '18 at 20:54
0

Given that your Storage class does not own the Thing objects, and every Thing object is uniquely counted, why not just store Thing* in the list?

class Storage {
 private:
   std::list<Thing*> list;
 public:
   void add(Thing& thing) {
     this->list.push_back(&thing);
   }
   Thing* findById(int id) const {
     for (auto thing : this->list) {
       if (thing->getId() == id) return thing;
     }
     std::cout << "Not found!!\n";
     return nullptr;
   }
};

EDIT: Note that Storage::findById now returns Thing* which allows it to fail gracefully by returning nullptr (rather than exit(1)).

Edy
  • 462
  • 3
  • 9