«Избегайте возврата дескрипторов внутренним объектам», так какова альтернатива?

Эффективный C ++ Скотт Мейерс в главе 5, пункт 28, говорит, что не следует возвращать «дескрипторы» (указатели, ссылки или итераторы) внутренним объектам, и это, безусловно, хорошо.

То есть не делай этого:

class Family
{
public:
Mother& GetMother() const;
}

потому что это разрушает инкапсуляцию и позволяет изменять элементы частного объекта.

Даже не делай этого:

class Family
{
public:
const Mother& GetMother() const;
}

потому что это может привести к «висящим маркерам», что означает, что вы сохраняете ссылку на член объекта, который уже уничтожен.

Теперь мой вопрос: есть ли хорошие альтернативы? Представь, что мама тяжелая! Если я теперь верну копию Mother вместо ссылки, GetMother становится довольно дорогой операцией.

Как вы справляетесь с такими случаями?

13

Решение

Во-первых, позвольте мне повторить: самая большая проблема заключается не в жизни, а в инкапсуляции.

Инкапсуляция означает не только то, что никто не может изменить внутреннее содержимое без вашего ведома, инкапсуляция означает, что никто не знает, как все реализовано в вашем классе, так что вы можете изменять внутреннее содержимое класса по своему желанию, пока вы сохраняете интерфейс идентичным.

Теперь, является ли ссылка, которую вы возвращаете const или не имеет значения: вы случайно выставляете тот факт, что у вас есть Mother объект внутри вашего Family класс, и теперь вы просто не можете избавиться от него (даже если у вас есть лучшее представление), потому что все ваши клиенты могут зависеть от него и должны будут изменить свой код …

Самое простое решение — вернуть по значению:

class Family {
public:

Mother mother() { return _mother; }
void mother(Mother m) { _mother = m; }

private:
Mother _mother;
};

Потому что в следующей итерации я могу удалить _mother не нарушая интерфейс:

class Family {
public:

Mother mother() { return Mother(_motherName, _motherBirthDate); }

void mother(Mother m) {
_motherName = m.name();
_motherBirthDate = m.birthDate();
}

private:
Name _motherName;
BirthDate _motherBirthDate;
};

Видите, как мне удалось полностью переделать внутренности, не меняя интерфейс ни на йоту? Очень просто.

Примечание: очевидно, что это преобразование только для эффекта …

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

14

Другие решения

Возможные решения зависят от фактического дизайна ваших классов и от того, что вы считаете «внутренними объектами».

  1. Mother это просто деталь реализации Family и может быть полностью скрыт от Family пользователь
  2. Family рассматривается как состав других общественных объектов

В первом случае вы должны полностью инкапсулировать подобъект и предоставлять доступ к нему только через Family члены функции (возможно дублирование Mother открытый интерфейс):

class Family
{
std::string GetMotherName() const { return mommy.GetName(); }
unsigned GetMotherAge() const { return mommy.GetAge(); }
...
private:
Mother mommy;
...
};

Ну, это может быть скучно, если Mother Интерфейс довольно большой, но, возможно, это проблема дизайна (у хороших интерфейсов должно быть 3-5-7 членов), и это заставит вас вернуться к нему и сделать его немного лучше.

Во втором случае вам все еще нужно вернуть весь объект. Есть две проблемы:

  1. Разрыв инкапсуляции (код конечного пользователя будет зависеть от Mother определение)
  2. Проблема собственности (висячие указатели / ссылки)

Для решения проблемы 1 используйте интерфейс вместо определенного класса, для решения проблемы 2 используйте общее или слабое владение:

class IMother
{
virtual std::string GetName() const = 0;
...
};

class Mother: public IMother
{
// Implementation of IMother and other stuff
...
};

class Family
{
std::shared_ptr<IMother> GetMother() const { return mommy; }
std::weak_ptr<IMother> GetMotherWeakPtr() const { return mommy; }

...
private:
std::shared_ptr<Mother> mommy;
...
};
5

Если вы ищете только для чтения, и по какой-то причине вам нужно избегать висячих ручек, то вы можете рассмотреть возможность возврата shared_ptr<const Mother>,

Таким образом, Mother объект может пережить Family объект. Который также должен хранить его shared_ptr, конечно.

Отчасти вопрос заключается в том, собираетесь ли вы создавать ссылочные циклы, используя слишком много shared_ptrs. Если да, то вы можете рассмотреть weak_ptr и вы можете также рассмотреть возможность принятия возможности висячих дескрипторов, но написания клиентского кода, чтобы избежать этого. Например, никто не слишком беспокоится о том, что std::vector::at возвращает ссылку, которая становится устаревшей при уничтожении вектора. Но тогда контейнеры являются крайним примером класса, который преднамеренно выставляет объекты, которыми он «владеет».

4

Это восходит к фундаментальному принципу ОО:

Tell objects what to do rather than doing it for them.

Тебе нужно Mother сделать что-нибудь полезное? Спроси Family возражать сделать это для вас. Дайте ему любые внешние зависимости, завернутые в приятный интерфейс (Class в с ++) через параметры метода на Family объект.

3

потому что это может привести к «висящим ручкам», что означает, что вы держите
ссылка на член объекта, который уже уничтожен.

Ваш пользователь также может отменить ссылку null или что-то столь же глупое, но они не собираются, и при этом они не собираются делать это, пока жизнь ясна и определена. В этом нет ничего плохого.

2

Это просто вопрос семантики. В твоем случае, Mother является не Family внутренности, а не детали ее реализации. Mother На экземпляр класса можно ссылаться в FamilyКак и во многих других организациях. Более того, Mother время жизни экземпляра может даже не соотноситься с Family продолжительность жизни.

Так что лучший дизайн будет хранить в Family shared_ptr<Mother>и выставить его в Family интерфейс без забот.

0
По вопросам рекламы [email protected]