class MyContainedClass {
};
class MyClass {
public:
MyContainedClass * getElement() {
// ...
std::list<MyContainedClass>::iterator it = ... // retrieve somehow
return &(*it);
}
// other methods
private:
std::list<MyContainedClass> m_contained;
};
Хотя msdn говорит, что std::list не должен выполнять перемещение элементов при удалении или вставке, является ли это хорошим и распространенным способом возврата указателя на элемент списка?
PS: Я знаю, что могу использовать сбор указателей (и мне нужно будет использовать элементы delete в деструкторе), сбор общих указателей (что мне не нравится) и т. д.





Я думаю, что более серьезная проблема заключается в том, что вы скрываете тип коллекции, поэтому, даже если вы используете коллекцию, которая не перемещает элементы, вы можете изменить свое мнение в будущем. Внешне этого не видно, поэтому я бы сказал, что это не лучшая идея.
Что же тогда инкапсулирует созданный вами класс? Я хочу сказать, что инкапсуляция, которую вы пытаетесь достичь, инкапсуляция коллекции, которая уже инкапсулирована STL, сродни тому, что я просил.
Я не вижу смысла инкапсулировать это, но это может быть только я. В любом случае для меня гораздо больше смысла возвращать ссылку вместо указателя.
В общем случае, если ваш «содержащийся класс» действительно содержится в вашем «MyClass», то MyClass не должен позволять посторонним трогать его личное содержимое.
Итак, MyClass должен предоставлять методы для управления содержащимися в нем объектами класса, не возвращая на них указатели. Так, например, такой метод, как «увеличить значение сотого содержащегося объекта», а не «здесь указатель на сотый содержащийся объект, делайте с ним, как хотите».
Это зависит от того, насколько инкапсулированным вы хотите, чтобы ваш класс был, и что вы хотите скрыть или показать.
Код, который я вижу, мне подходит. Вы правы в том, что данные и итераторы std :: list не будут аннулированы в случае изменения / удаления других данных / итератора.
Теперь возврат указателя скроет тот факт, что вы используете std :: list в качестве внутреннего контейнера, и не позволит пользователю перемещаться по его списку. Возврат итератора предоставит пользователям класса больше свободы в навигации по этому списку, но они будут «знать», что обращаются к контейнеру STL.
Думаю, это твой выбор.
Обратите внимание, что если это == std :: list <>. End (), то у вас возникнут проблемы с этим кодом, но я думаю, вы это уже знаете, и это не является предметом данного обсуждения.
Тем не менее, есть альтернатива, которую я резюмирую ниже:
const поможет ...Тот факт, что вы возвращаете неконстантный указатель, позволяет пользователю вашего объекта молча изменять любой MyConolatedClass, который он / она может получить в свои руки, не сообщая вашему объекту.
Вместо этого или возвращая указатель, вы можете вернуть указатель const (и суффикс вашего метода с помощью const), чтобы пользователь не мог изменять данные внутри списка без использования одобренного вами средства доступа (своего рода setElement?).
const MyContainedClass * getElement() const {
// ...
std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
return &(*it);
}
Это несколько увеличит инкапсуляцию.
Если ваш метод не может дать сбой (т.е. он всегда возвращает действительный указатель), вам следует рассмотреть возможность возврата ссылки вместо указателя. Что-то вроде:
const MyContainedClass & getElement() const {
// ...
std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
return *it;
}
Однако это не имеет ничего общего с инкапсуляцией .. :-п
Почему бы не вернуть итератор вместо указателя? Если для вас перемещение по списку вверх и вниз нормально, то итератор будет лучше, чем указатель, и используется в основном таким же образом.
Сделайте итератор const_iterator, если вы хотите избежать изменения данных пользователем.
std::list<MyContainedClass>::const_iterator getElement() const {
// ...
std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
return it;
}
Хорошей стороной будет то, что пользователь сможет перемещаться по списку. Плохая сторона в том, что пользователь будет знать, что это std :: list, поэтому ...
Скотт Мейерс в своей книге Эффективный STL: 50 конкретных способов улучшить использование стандартной библиотеки шаблонов говорит, что просто не стоит пытаться инкапсулировать ваши контейнеры, поскольку ни один из них не может быть полностью заменен другим.
std :: list не аннулирует какие-либо итераторы, указатели или ссылки при добавлении или удалении элементов из списка (очевидно, кроме любой точки удаляемого элемента), поэтому использование списка таким образом не приведет к поломке.
Как указывали другие, вы можете не захотеть предоставлять прямой доступ к частным битам этого класса. Итак, изменив функцию на:
const MyContainedClass * getElement() const {
// ...
std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
return &(*it);
}
может быть лучше, или если вы всегда возвращаете действительный объект MyContainClass, вы можете использовать
const MyContainedClass& getElement() const {
// ...
std::list<MyContainedClass>::const_iterator it = ... // retrieve somehow
return *it;
}
чтобы вызывающий код не работал с указателями NULL.
STL будет более знакома будущему программисту, чем ваша индивидуальная инкапсуляция, поэтому вам следует по возможности избегать этого. Будут крайние случаи, о которых вы не задумывались, которые возникнут позже в течение жизненного цикла приложения, когда STL хорошо изучен и задокументирован.
Кроме того, большинство контейнеров поддерживают несколько схожие операции, такие как начало и конец нажатия и т. д. Таким образом, изменение типа контейнера в вашем коде должно быть довольно тривиальным делом, если вы измените контейнер. например, вектор для deque или отображение в hash_map и т. д.
Предполагая, что вы все еще хотите сделать это по более глубокой причине, я бы сказал, что правильный способ сделать это - реализовать все методы и классы итераторов, которые реализует список. Если вам не нужно ничего менять, перенаправляйте вызовы на вызовы из списка участников. Измените и отправьте или выполните некоторые настраиваемые действия, когда вам нужно сделать что-то особенное (причина, по которой вы решили это в первую очередь)
Было бы проще, если бы классы STl были спроектированы для наследования, но ради эффективности было решено этого не делать. Google для "наследования от классов STL", чтобы узнать больше об этом.
Подумайте хорошо и тяжело о том, для чего вы действительно хотите MyClass. Я заметил, что некоторые программисты пишут обертки для своих коллекций просто по привычке, независимо от того, есть ли у них какие-либо особые потребности помимо тех, которые удовлетворяются стандартными коллекциями STL. Если это ваша ситуация, то typedef std::list<MyContainedClass> MyClass и покончим с этим.
Если у вас делать есть операции, которые вы собираетесь реализовать в MyClass, то успех вашей инкапсуляции будет больше зависеть от интерфейса, который вы им предоставляете, чем от того, как вы предоставляете доступ к базовому списку.
Нет смысла в обиде, но ... С той ограниченной информацией, которую вы предоставили, это пахнет, как будто вы таращитесь: раскрытие внутренних данных, потому что вы не можете понять, как реализовать операции, требуемые вашим клиентским кодом в MyClass ... или возможно, потому что вы еще даже не знаете знать, какие операции потребуются вашему клиентскому коду. Это классическая проблема при попытке написать код низкого уровня раньше кода высокого уровня, который этого требует; вы знаете, с какими данными вы будете работать, но еще не определили, что именно вы будете с ними делать, поэтому вы пишете структуру классов, которая полностью раскрывает необработанные данные. Вам следует пересмотреть свою стратегию здесь.
@cos:
Of course I'm encapsulating MyContainedClass not just for the sake of encapsulation. Let's take more specific example:
Ваш пример мало помогает развеять мои опасения, что вы пишете свои контейнеры до того, как узнаете, для чего они будут использоваться. Ваш пример оболочки контейнера - Document - имеет в общей сложности три метода: NewParagraph(), DeleteParagraph() и GetParagraph(), каждый из которых работает с содержащейся коллекцией (std::list), и все это точно отражает операции, которые std::list предоставляет «из коробки». Document инкапсулирует std :: list в том смысле, что клиентам не нужно знать о его использовании в реализации ... но на самом деле это немного больше, чем фасад - поскольку вы предоставляете клиентам необработанные указатели на объекты, хранящиеся в списке, клиент по-прежнему неявно привязан к реализации.
If we put objects (not pointers) to container they will be destroyed automatically (which is good).
Хорошее или плохое зависит от потребностей вашей системы. Эта реализация означает просто: документу принадлежат Paragraph, и когда Paragraph удаляется из документа, любые указатели на него немедленно становятся недействительными. Это означает, что вы должны быть осторожны с очень при реализации чего-то вроде:
other objects than use collections of paragraphs, but don't own them.
Теперь у тебя проблема. Ваш объект ParagraphSelectionDialog имеет список указателей на объекты Paragraph, принадлежащие Document. Если вы не будете внимательно координировать эти два объекта, Document - или другой клиент посредством Document - может аннулировать некоторые или все указатели, содержащиеся в экземпляре ParagraphSelectionDialog! Нет простого способа уловить это - указатель на действительный Paragraph выглядит так же, как указатель на освобожденный Paragraph, и может даже в конечном итоге указывать на действительный, но другой экземпляр Paragraph! Поскольку клиентам разрешено (и даже ожидается) сохранять и разыменовывать эти указатели, Document теряет контроль над ними, как только они возвращаются из общедоступного метода, даже если он сохраняет право собственности на объекты Paragraph.
Это плохо. Вы получили неполную, поверхностную инкапсуляцию, дырявую абстракцию, и в некотором смысле это хуже, чем полное отсутствие абстракции. Поскольку вы скрываете реализацию, ваши клиенты не имеют представления о времени жизни объектов, на которые указывает ваш интерфейс. В большинстве случаев вам, вероятно, повезет, поскольку большинство операций std::list не делают недействительными ссылки на элементы, которые они не изменяют. И все будет хорошо ... до тех пор, пока не будет удален неправильный Paragraph, и вы не обнаружите, что столкнетесь с задачей отслеживания стека вызовов в поисках клиента, который слишком долго держал этот указатель.
Исправление достаточно простое: возвращайте значения или объекты, которые можно хранить столько, сколько нужно, и проверять перед использованием. Это может быть что-то столь же простое, как порядковый номер или значение идентификатора, которое должно быть передано в Document в обмен на полезную ссылку, или такое же сложное, как умный указатель с подсчетом ссылок или слабый указатель ... это действительно зависит от конкретных потребностей ваши клиенты. Сначала укажите код клиента, а затем напишите свой Document для обслуживания.
спасибо за комментарии. Я должен об этом подумать. В любом случае я пишу «// другие свойства» в документе, что означает, что у него есть другие поля, в которых хранятся его конкретные значения, например. цвет страницы, размер страницы и т. д.
Легкий способ
@cos, В примере, который вы показали, я бы сказал, что самый простой способ создать эту систему на C++ - не беспокоиться о подсчете ссылок. Все, что вам нужно сделать, это убедиться, что поток программы сначала уничтожает объекты (представления), которые содержат прямые ссылки на объекты (абзацы) в коллекции, прежде чем корневой документ будет уничтожен.
Трудный путь
Однако, если вы по-прежнему хотите контролировать время жизни путем отслеживания ссылок, вам, возможно, придется хранить ссылки глубже в иерархии, чтобы объекты Paragraph содержали обратные ссылки на корневой объект Document, так что только когда последний объект абзаца будет уничтожен, объект Document быть разрушенным.
Кроме того, ссылки на абзацы при использовании внутри класса Views и при передаче другим классам также должны будут передаваться как интерфейсы с подсчетом ссылок.
Стойкость
Это слишком много накладных расходов по сравнению с простой схемой, которую я перечислил в начале. Это позволяет избежать всех видов накладных расходов на подсчет объектов и, что более важно, тот, кто наследует вашу программу, не попадает в ловушки ссылочных потоков зависимостей, которые пересекают вашу систему.
Альтернативные платформы
Этот вид инструментов может быть проще реализовать на платформе, которая поддерживает и продвигает этот стиль программирования, например .NET или Java.
Тебе все еще нужно беспокоиться о памяти
Даже с такой платформой, как эта, вам все равно придется гарантировать, что ссылки на ваши объекты будут прекращены должным образом. Остальные выдающиеся ссылки могут съесть вашу память в мгновение ока. Итак, вы видите, что подсчет ссылок - не панацея от хороших практик программирования, хотя он помогает избежать множества проверок ошибок и чисток, что при применении всей системы значительно облегчает задачу программистов.
Рекомендация
Тем не менее, возвращаясь к вашему исходному вопросу, который вызвал все сомнения при подсчете ссылок - можно ли выставлять ваши объекты прямо из коллекции?
Программы не могут существовать, если все классы / все части программы действительно взаимозависимы друг от друга. Нет, это невозможно, поскольку программа - это работающее проявление взаимодействия ваших классов / модулей. Идеальный дизайн может только минимизировать зависимости, но не удалить их полностью.
Так что мое мнение было бы, да, это неплохая практика - выставлять ссылки на объекты из вашей коллекции другим объектам, которые должны с ними работать, при условии, что вы делаете это разумным образом
Убедитесь, что только несколько классов / частей вашей программы могут получить такие ссылки, чтобы гарантировать минимальную взаимозависимость.
Убедитесь, что переданные ссылки / указатели являются интерфейсами, а не конкретными объектами, чтобы избежать взаимозависимости между конкретными классами.
Убедитесь, что ссылки не передаются дальше в программу.
Убедитесь, что логика программы заботится об уничтожении зависимых объектов, прежде чем очищать фактические объекты, удовлетворяющие этим ссылкам.
Я думал, что сокрытие фактического метода хранения и организации объектов является основной целью инкаплюции (и, например, я могу изменить хранилище списков на доступ к базе данных, и другие внешние объекты не почувствуют разницы)