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

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

т.е. не делай этого:

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

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

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

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

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

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

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


person schluchc    schedule 01.11.2012    source источник
comment
Это зависит от причины, по которой я хочу получить доступ к этой штуке...   -  person PlasmaHH    schedule 01.11.2012
comment
Я так стараюсь не пытаться острить Мать тяжелая :P   -  person Lews Therin    schedule 01.11.2012
comment
Возможно, std::shared_ptr<Mother>?   -  person juanchopanza    schedule 01.11.2012
comment
На самом деле вы упустили суть. Это не столько проблема времени жизни (хотя есть и это), гораздо важнее нарушение инкапсуляции: и что, если вы захотите заменить этот класс Mother на класс MotherImpl (более легкий) в какой-то момент в Family реализация ? Как вы теперь выполняете контракт GetMother?   -  person Matthieu M.    schedule 01.11.2012
comment
@DeadMG: Ну, я бы сказал, что YAGNI применяется к возврату по shared_ptr, это, безусловно, намного сложнее, чем просто возврат по копии ;)   -  person Matthieu M.    schedule 01.11.2012
comment
@LewsTherin: его мать такая тяжелая, но НАСТОЛЬКО тяжелая, что она хотела банан, и функция вернула горилла ДЕРЖИТ банан...   -  person heltonbiker    schedule 03.09.2014


Ответы (6)


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

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

Теперь не имеет значения, является ли ссылка, которую вы возвращаете, 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;
};

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

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

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

person Matthieu M.    schedule 01.11.2012

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

  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;
   ...
};
person Rost    schedule 01.11.2012
comment
GetWeakMother мама не слабая! - person Matthieu M.; 01.11.2012
comment
@MatthieuM. Изменил на GetMotherByWeakPtr, просто для вашего удовольствия :-D - person Rost; 01.11.2012

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

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

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

person Steve Jessop    schedule 01.11.2012
comment
Вы имеете в виду shared_ptr<const Mother>? - person kennytm; 01.11.2012
comment
Я бы не рекомендовал это. Хотя это решает насущную проблему висячей ссылки, это увеличивает стоимость и сложность дизайна, которые, я думаю, не нужны. Время жизни объектов в C++ — это не то, что можно слепо игнорировать, просто научитесь с этим жить. Задокументируйте, каковы гарантии действительности возвращенной ссылки, и двигайтесь дальше :) - person David Rodríguez - dribeas; 01.11.2012
comment
@DavidRodríguez-dribeas: вы бы не рекомендовали рассматривать это, или вы считаете, что рассмотрение обычно примет решение против shared_ptr? Я согласен с последним, а не с первым. В этом отношении я бы также поставил под сомнение мое самое первое предположение, если представление только для чтения - это то, что вам нужно, прежде чем все усложнять. Возврат по значению — это хорошо, и если производительность является проблемой, то есть такие варианты, как неизменяемые типы или COW, которые следует рассмотреть, прежде чем прибегать к представлению вместо значения. - person Steve Jessop; 01.11.2012
comment
Короче говоря, каждый раз, когда вы думаете, что хотите нарушить совет Мейерса и нуждаетесь в альтернативе, может быть проблема XY, и множество ответов здесь пытаются решить X различными способами. - person Steve Jessop; 01.11.2012
comment
Я вижу в этом еще одно воплощение цитаты Йоги Берра: В теории теория и практика одинаковы. На практике их нет (добавьте в тот же набор, например NVI). Да, в теории это улучшает дизайн, на практике люди просто этого не делают, а в некоторых случаях это может реально все усложнить. (И да, я бы не рекомендовал делать это, учитывать всегда полезно :) - person David Rodríguez - dribeas; 01.11.2012

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

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

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

person Chris Pfohl    schedule 01.11.2012
comment
Следовательно, вы получаете функции printMotherName, printFatherName, printMaternalGrandfatherName и т. д. Иногда, конечно, можно срезать эти чащи с помощью полезной абстракции, такой как printNameOfRelative(RelationShip). - person Steve Jessop; 01.11.2012
comment
И с такими структурами становится все сложнее, потому что в графе может быть много дочерних элементов. printSonName() не работает, если есть 74 сына, поэтому вам нужна перегрузка, которая принимает индекс, а также средство получения счетчика, но папа мог жениться повторно, поэтому вам может понадобиться то же самое для printMotherName()... это быстро становится грязным. - person Jonathan Grynspan; 01.11.2012
comment
Чтобы упростить, я бы сказал, что если ваш клиентский код выглядит как обход графа, тогда вы, возможно, сможете заставить работать шаблон Посетитель, но в противном случае фундаментальный принцип ООП, вероятно, неприменим. Существует концептуальная разница между семьей, имеющей мать и контролирующей весь доступ к этой матери со стороны клиентов интерфейса Family, и семьей и матерью, являющимися достаточно независимыми объектами, чье отношение друг к другу может наблюдаться кодом, который является клиентом обоих. Семейный интерфейс и Материнский интерфейс. - person Steve Jessop; 01.11.2012
comment
Да, и с энтузиазмом применяемый Закон Деметры запрещает file.getLine(0).rstrip() на том основании, что пользователи интерфейса File не должны таким образом связываться с интерфейсом String. Поэтому вместо этого вы пишете file.getrstrippedLine(0). Вы должны принять решение, когда пользователям семейного интерфейса целесообразно избегать также использования материнского интерфейса. И если это практично, сделайте это. - person Steve Jessop; 01.11.2012
comment
Я, конечно, ни с чем здесь не согласен. Не нужно слишком усложнять. Я просто указал способ устранить проблему и убедиться, что все остается хорошо и развязано. Мой код изобилует двойными пунктирными линиями, и мне хочется верить, что его чертовски удобно поддерживать. - person Chris Pfohl; 01.11.2012

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

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

person Puppy    schedule 01.11.2012
comment
Это становится не столь очевидным, если фактический Mother потребитель не является кодом, который его приобретает. Когда, например. он проходит через некоторый нетривиальный стек вызовов. - person Rost; 01.11.2012
comment
Один из способов сделать время жизни ясным и четко определенным — это, конечно, не раздавать ссылки на рассматриваемый объект волей-неволей ;-) - person Rook; 01.11.2012

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

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

person Igor R.    schedule 01.11.2012