我有一个Map
类,其中包含Storage
的列表:
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);
}
};
开始,然后在插入和检索时复制了所有内容,我不想要这个,因为如果我得到一个副本,改变它就不会反映原始对象了。在寻找解决方案时,我发现了std::list<Thing>
on this SO question,但现在我遇到了另一个问题。
现在使用它们的代码:
std::reference_wrapper
我的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";
}
只是致电main()
。我得到的输出是:
run()
虽然我在寻找:
50
10101
Not found!!
当函数返回时,看起来本地声明的对象50
10101
50
不再存在,这是有道理的。我可以通过使用t4
动态分配它来阻止这种情况,但后来我不想手动管理内存...
如何在不删除new
功能且无需手动管理内存的情况下修复代码?
如果我只是按照某些建议使用temp()
,那么std::list<Thing>
和t4
的问题肯定会不复存在,但会出现另一个问题:代码赢得了&#t; t例如,再打印temp
。如果我一直在复制东西,我就无法改变存储对象的状态。
答案 0 :(得分:7)
谁是存储中Thing的所有者?
您的实际问题是所有权。目前,您的Storage
并不真正包含Things
,而是留给Storage
的用户来管理您放入其中的对象的生命周期。这与std容器的哲学非常相反。所有standard C++ containers拥有您放入其中的对象,容器管理它们的生命周期(例如,您只需在向量上调用v.resize(v.size()-2)
并最后两个元素被销毁)。
为何参考?
您已经找到了一种方法来使容器不拥有实际对象(使用reference_wrapper
),但没有理由这样做。在一个名为Storage
的类中,我希望它能保存对象而不仅仅是引用。而且,这为许多令人讨厌的问题打开了大门,包括未定义的行为。例如:
void temp(Storage& storage) {
storage.findById(2).add(1);
Thing t4; t4.add(50);
storage.add(t4);
std::cout << storage.findById(4).getValue() << "\n";
}
您在t4
中存储对storage
的引用。问题是:t4
的生命周期只有在该函数结束时才会出现悬空参考。你可以存储这样的引用,但它不是很有用,因为你基本上不允许用它做任何事情。
Aren引用了一件很酷的事情?
目前您可以推送t1
,修改它,然后在Storage
中观察对thingy的更改,如果您想模仿Java,这可能没问题,但在c ++中我们习惯于容器当你推东西时制作一个副本(还有一些方法来创建元素到位,以防你担心一些无用的临时工)。是的,当然,如果你真的想要你可以制作一个标准的容器也可以容纳参考,但是让我们绕道而行......
谁收集了所有垃圾?
在C ++具有析构函数时,考虑Java是垃圾收集可能会有所帮助。在Java中,您习惯于引用浮动,直到垃圾收集器启动。在C ++中,您必须非常了解对象的生命周期。这可能听起来很糟糕,但实际上对于完全控制对象的生命周期非常有用。
<强>垃圾?什么垃圾?
在现代C ++中,你不应该忘记忘记delete
,而是要体会到RAII的优点。获取初始化资源并了解析构函数何时被调用可以获得基本上任何类型资源的自动资源管理,垃圾收集器只能梦想(想想文件,数据库连接等)。
&#34;如何在不删除temp()函数的情况下修复代码,而无需手动管理内存?&#34;
这对我有很大帮助的一个技巧是:每当我发现自己需要手动管理资源时,我就会停下来询问“其他人不能做脏事吗?”#34;。真的非常罕见,我找不到一个标准容器,它完全符合我的需要。在你的情况下,让std::list
做&#34;脏&#34;工作。
如果没有模板,就不能成为C ++,对吗?
我实际上建议你将Storage
作为模板,沿着以下方式:
template <typename T>
class Storage {
private:
std::list<T> list;
//....
然后
Storage<Thing> thing_storage;
Storage<int> int_storage;
分别是包含Storage
和Thing
的{{1}}个。这样,如果您想要使用引用或指针进行实验,您仍然可以实例化int
。
我错过了什么吗?......可能是参考文献?
我无法改变存储对象的状态
鉴于容器拥有该对象,您宁愿让用户对容器中的对象进行引用。例如,使用
的向量Storage<reference_wrapper<int>>
要使您的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
能够正常使用,您必须让Storage
返回引用。作为演示:
findById
<强> TL; DR 强>
如何避免手动资源管理?让别人为你做,并称之为自动资源管理:P
答案 1 :(得分:1)
t4
是一个临时对象,在temp()
退出时会被销毁,而您在storage
中存储的内容会成为悬空引用,从而导致UB。
目前还不是很清楚你想要实现的目标,但是如果你想让Storage
类与它保持一致,你应该确保存储在其中的所有引用都在至少与storage
本身一样长寿。您发现这是STL容器保留其元素的私有副本的原因之一(其他可能不那么重要,在某些情况下消除了额外的间接和更好的位置)。
P.S。并且,请你停止编写那些this->
并了解构造函数中的初始化列表吗? &GT; _&LT;
答案 2 :(得分:1)
根据我的估计,就您的代码实际看起来要做的事情而言,您的代码肯定过于复杂。考虑一下这段代码,它可以完成您的代码所做的所有相同的事情,但使用的样本代码少得多,而且使用方式更加安全:
#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
请注意,这里发生了一些有趣的事情:
t2
是引用,并且插入地图并不会使引用无效,因此可以修改t2
,并且这些修改将反映在地图本身中,反之亦然things
拥有插入其中的所有值,并且由于RAII和std::map
的内置行为以及它所遵循的更广泛的C ++设计原则而将被清除。没有人担心物体没有被清理干净。如果您需要保留自动处理id
递增的行为,而不是最终程序员,我们可以考虑使用此代码:
#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
这些代码段应该让您对语言的工作方式有一个体面的认识,以及您需要了解的代码中可能不熟悉的原则。我的一般建议是找到一个很好的C ++资源来学习该语言的基础知识,并从中学习。一些好的资源can be found here。
最后一件事:如果使用Thing
对您的代码至关重要,因为您需要在地图中保存更多数据,请考虑改为:
#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;
}
答案 3 :(得分:1)
基于FrançoisAndrieux和Eljay所说的(以及我会说的,如果我先到达那里),这就是我要做的方式, if 你想要改变对象你已添加到列表中。所有reference_wrapper
内容只是传递指针的一种奇特方式。它会以泪水结束。
行。这是代码(现在根据OP的请求编辑):
#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;
}
现在输出:
50
10101
50
根据需要。在Wandbox上运行。
请注意,您在此处执行的操作实际上是引用计数您的Thing
。 Thing
本身永远不会被复制,并且会在最后shared_ptr
超出范围时消失。仅复制shared_ptr
,并且设计以进行复制,因为这是他们的工作。以这种方式做事几乎和传递引用(或包装引用)一样有效,并且更安全。在开始时,很容易忘记引用只是伪装的指针。
答案 4 :(得分:0)
鉴于您的Storage
类不拥有 Thing
个对象,并且每个Thing
对象都是唯一计数的,为什么不只是存储Thing*
} 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;
}
};
编辑:请注意Storage::findById
现在返回Thing*
,这样可以通过返回nullptr
(而不是exit(1)
)来优雅地失败。