Можно ли избежать управления памятью вручную в этой ситуации в C ++? - PullRequest
0 голосов
/ 01 июня 2018

У меня есть класс 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>, но затем все копируется при вставке и извлечении, и яЯ не хотел этого, потому что, если я получу копию, ее изменение больше не будет отражаться на оригинальных объектах.При поиске решения этой проблемы я нашел около std::reference_wrapper в этом вопросе SO , но теперь у меня возникла другая проблема.

Теперь к коду, который их использует:

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 и temp перестанет существовать, но возникнет другая проблема: код не будет печатать 10101, например.Если я продолжу копировать вещи, я не смогу изменить состояние хранимого объекта.

Ответы [ 5 ]

0 голосов
/ 02 июня 2018

Учитывая, что ваш 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)).

0 голосов
/ 01 июня 2018

Исходя из того, что сказали Франсуа Андрие и Эджей (и что бы я сказал, если бы я добрался туда первым), вот способ, которым я бы это сделал, , если вы хотите видоизменить ваши объектыуже добавлен в список.Все эти 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, и они предназначены для копирования , потому что это их работа.Делать вещи таким способом почти так же эффективно, как передавать ссылки (или свернутые ссылки) и гораздо безопаснее.Когда вы начинаете, легко забыть, что ссылка - это просто замаскированный указатель.

0 голосов
/ 01 июня 2018

t4 - это временный объект, который уничтожается при выходе из temp(), и то, что вы храните в storage, становится висячей ссылкой, вызывая UB.

Не совсем понятно, что выпытаясь достичь этого, но если вы хотите сохранить класс Storage таким же, как он есть, вы должны убедиться, что все ссылки, хранящиеся в нем, по крайней мере так же долговечны, как и сам storage.Вы обнаружили, что это одна из причин, по которой контейнеры STL сохраняют свои частные копии элементов (другие, возможно, менее важные, - исключение дополнительной косвенности и, в некоторых случаях, гораздо лучшую локальность).

PS И, пожалуйста,Можете ли вы прекратить писать эти this-> и узнать о списках инициализации в конструкторах?> _ <</p>

0 голосов
/ 01 июня 2018

Что касается того, что на самом деле делает ваш код, то, по моим оценкам, вы явно усложнили свой код.Рассмотрим этот код, который выполняет все те же действия, что и ваш код, но с гораздо меньшим количеством стандартного кода и способом, который намного более безопасен для вашего использования:

#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 ++ ресурс для изучения основ языка и учиться на этом.Некоторые хорошие ресурсы можно найти здесь .

И последнее: если использование 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;
}
0 голосов
/ 01 июня 2018

Кто является владельцем вещи в хранилище?

Ваша настоящая проблема - право собственности.В настоящее время ваш Storage на самом деле не содержит Things, но вместо этого он оставлен на усмотрение пользователя Storage для управления временем жизни объектов, которые вы помещаете в него.Это очень сильно противоречит философии стандартных контейнеров.Все стандартные контейнеры C ++ владеют объектами, которые вы в них помещаете, и контейнер управляет их временем жизни (например, вы просто вызываете 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 время жизни только до конца этой функции, и вы получите висячую ссылку.Вы можете хранить такую ​​ссылку, но это не очень полезно, потому что вам, по сути, не разрешено с ней ничего делать.

Разве ссылки не полезны?

В настоящее время вы можете нажать t1, изменить его, а затем заметить, что изменения в вещи в Storage могут быть хорошими, если вы хотите имитировать Java, но в c ++ мы привыкли к контейнерам, делающим копию, когда вы что-то нажимаете(Существуют также методы для создания элементов на месте, если вы беспокоитесь о каких-то бесполезных временных).И да, конечно, если вы действительно хотите, вы можете сделать стандартный контейнер, также содержащий ссылки, но давайте сделаем небольшой обход ...

Кто собирает весь этот мусор?

Может быть, полезно учитывать, что Java является сборщиком мусора, а в C ++ есть деструкторы.В Java вы привыкли к ссылкам, плавающим до тех пор, пока не сработает сборщик мусора. В C ++ вы должны быть супер осведомлены о времени жизни ваших объектов.Это может звучать плохо, но на самом деле оказывается чрезвычайно полезным иметь полный контроль над временем жизни объектов.

Мусор?Что за фигня?

В современном C ++ вы не должны беспокоиться о том, чтобы забыть delete, а скорее оцените преимущества наличия RAII .Получение ресурсов при инициализации и знание, когда вызывается деструктор, позволяет получить автоматическое управление ресурсами практически для любого типа ресурса, о чем сборщик мусора может только мечтать (думать о файлах, соединениях с базой данных и т. Д.).

«Как я могу исправить код, не удаляя функцию temp () и не управляя памятью вручную?»

Уловка, которая мне очень помогла, заключается в следующем: всякий раз, когда я думаю о себеМне нужно управлять ресурсом вручную, я останавливаюсь и спрашиваю: «Разве кто-то еще не может делать грязные вещи?».Это действительно очень редко, что я не могу найти стандартный контейнер, который делает именно то, что мне нужно из коробки.В вашем случае просто позвольте std::list сделать «грязную» работу.

Не может быть C ++, если нет шаблона, верно?

Я бы фактически предложил вам сделать Storage шаблоном, например:

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

Тогда

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

составляют Storage с, содержащие Thing с и 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 вернуть ссылку.В качестве демонстрации:

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

Как избежать ручного управления ресурсами?Пусть кто-нибудь другой сделает это за вас и назовет это автоматическим управлением ресурсами: P

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...