Что лучше: указатели или ссылки в данных-членах?


141

Это упрощенный пример, иллюстрирующий вопрос:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Итак, B отвечает за обновление части C. Я запустил код через lint, и он пожаловался на элемент ссылки: lint # 1725 . Здесь говорится о заботе о копировании и присваиваниях по умолчанию, что достаточно справедливо, но копирование и присваивание по умолчанию также плохо работают с указателями, поэтому здесь мало преимуществ.

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

Обычно я бы использовал указатель в данных члена только тогда, когда указатель мог быть нулевым или мог измениться. Есть ли другие причины предпочесть указатели ссылкам на элементы данных?

Верно ли сказать, что объект, содержащий ссылку, не должен быть назначен, поскольку ссылка не должна изменяться после инициализации?


«но копирование и присваивание по умолчанию также плохо с указателями»: это не то же самое: указатель может быть изменен в любое время, если он не является константой. Ссылка обычно всегда const! (Вы увидите это, если попытаетесь изменить свой член на «A & const a;». Компилятор (по крайней мере, GCC) предупредит вас, что ссылка в любом случае является const даже без ключевого слова «const».
мммммммм

Основная проблема заключается в том, что если кто-то выполняет B b (A ()), вы облажались, потому что в конечном итоге вы получаете висящую ссылку.
log0

Ответы:


69

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

Примеры проблем:

  • вы вынуждены инициализировать ссылку в каждом списке инициализаторов конструктора: нет возможности вынести эту инициализацию в другую функцию ( до C ++ 0x, в любом случае редактировать: C ++ теперь имеет делегирующие конструкторы )
  • ссылка не может быть восстановлена ​​или иметь значение NULL. Это может быть преимуществом, но если код когда-либо потребуется изменить, чтобы разрешить повторную привязку или чтобы член был нулевым, все виды использования члена должны измениться.
  • в отличие от членов-указателей, ссылки нельзя легко заменить интеллектуальными указателями или итераторами, поскольку для рефакторинга может потребоваться
  • Всякий раз, когда используется ссылка, она выглядит как тип значения ( .оператор и т. Д.), Но ведет себя как указатель (может болтаться) - поэтому, например, в Google Style Guide это не рекомендуется.

68
Все, что вы упомянули, следует избегать, поэтому, если ссылки помогают в этом - они хороши, а не плохи. Список инициализации - лучшее место для инициализации данных. Очень часто вам приходится скрывать оператор присваивания со ссылками, которые вам не нужны. «Невозможно отскочить» - хорошо, повторное использование переменных - плохая идея.
Николай Голубев

4
@Mykola: Я согласен с вами. Я предпочитаю, чтобы члены инициализировались в списках инициализаторов, я избегаю нулевых указателей и, конечно, нехорошо, чтобы переменная меняла свое значение. Наши мнения расходятся в том, что мне не нужно и не хочу, чтобы компилятор выполнял это за меня. Это не упрощает написание классов, я не сталкивался с ошибками в этой области, которые он мог бы обнаружить, и иногда я ценю гибкость, которую я получаю от кода, который последовательно использует элементы-указатели (интеллектуальные указатели, где это необходимо).
Джеймс Хопкин

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

4
Джеймс, дело не в том, что это упрощает написание кода, а в том, что это упрощает чтение кода. Имея ссылку в качестве члена данных, вам никогда не придется задаваться вопросом, может ли она быть нулевой в момент использования. Это означает, что вы можете смотреть на код с меньшим объемом контекста.
Лен Холгейт

4
Это делает модульное тестирование и имитацию намного более сложными, невозможными почти в зависимости от того, сколько их используется, в сочетании с «взрывом графа эффектов» - ИМХО, это достаточно веские причины, чтобы никогда их не использовать.
Крис Хуанг-Ливер,

160

Мое практическое правило:

  • Используйте ссылочный член, когда вы хотите, чтобы жизнь вашего объекта зависела от жизни других объектов : это явный способ сказать, что вы не позволяете объекту быть живым без действительного экземпляра другого класса - из-за отсутствия присваивание и обязательство получить инициализацию ссылок через конструктор. Это хороший способ спроектировать свой класс, не предполагая, что его экземпляр является членом другого класса или не принадлежит ему. Вы только предполагаете, что их жизни напрямую связаны с другими случаями. Это позволяет вам позже изменить способ использования экземпляра класса (новый, как локальный экземпляр, как член класса, сгенерированный пулом памяти в диспетчере и т. Д.)
  • Используйте указатель в других случаях : если вы хотите, чтобы член был изменен позже, используйте указатель или константный указатель, чтобы быть уверенным, что вы читаете только указанный экземпляр. Если предполагается, что этот тип копируемый, вы все равно не можете использовать ссылки. Иногда вам также необходимо инициализировать член после вызова специальной функции (например, init ()), и тогда у вас просто нет выбора, кроме как использовать указатель. НО: используйте утверждения во всех функциях-членах, чтобы быстро обнаружить неправильное состояние указателя!
  • В случаях, когда вы хотите, чтобы время жизни объекта зависело от времени жизни внешнего объекта, и вам также нужно, чтобы этот тип был копируемым, тогда используйте элементы-указатели, но ссылочный аргумент в конструкторе. Таким образом вы указываете на конструкцию, что время жизни этого объекта зависит на время жизни аргумента, НО реализация использует указатели, чтобы их можно было копировать. Пока эти члены изменяются только копированием, а ваш тип не имеет конструктора по умолчанию, тип должен соответствовать обеим целям.

1
Я думаю, что ваш и Микола - правильные ответы и продвигают программирование без указателя (без указателя).
amit

37

Объекты редко должны разрешать присваивание и другие вещи, такие как сравнение. Если рассматривать какую-то бизнес-модель с такими объектами, как «Отдел», «Сотрудник», «Директор», трудно представить себе случай, когда один сотрудник будет закреплен за другим.

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

И, вероятно, нормально описывать отношение «один или ноль» как указатель.

Так что никакого фактора «мы не можем назначить».
Многие программисты просто привыкли к указателям, и поэтому они найдут любой аргумент, чтобы избежать использования ссылки.

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


2
+1: То же, что и я. Только некоторые общие классы хранения данных нуждаются в assignemtn и copy-c'tor. А для более высокоуровневых структур бизнес-объектов должны быть другие методы копирования, которые также обрабатывают такие вещи, как «не копировать уникальные поля» и «добавлять к другому родительскому элементу» и т. Д. Таким образом, вы используете высокоуровневую структуру для копирования своих бизнес-объектов. и запретить низкоуровневые назначения.
мммммммм

2
+1: Некоторые моменты согласия: ссылочные члены, препятствующие определению оператора присваивания, не являются важным аргументом. Как вы правильно утверждаете, не все объекты имеют семантику значений. Я также согласен с тем, что мы не хотим, чтобы «if (p)» было разбросано по всему коду без какой-либо логической причины. Но правильный подход к этому - через инварианты классов: четко определенный класс не оставляет места для сомнений в том, могут ли члены быть нулевыми. Если какой-либо указатель в коде может иметь значение NULL, я ожидаю, что это будет хорошо прокомментировано.
Джеймс Хопкин

@JamesHopkin Я согласен с тем, что хорошо спроектированные классы могут эффективно использовать указатели. Помимо защиты от NULL, ссылки также гарантируют, что ваша ссылка была инициализирована (указатель не нужно инициализировать, поэтому он может указывать на что-нибудь еще). Ссылки также говорят вам о владении, а именно о том, что объект не владеет членом. Если вы последовательно используете ссылки для составных элементов, вы будете очень уверены в том, что элементы-указатели являются составными объектами (хотя не так много случаев, когда составной член представлен указателем).
weberc2

11

7
Я читал это, но полагал, что речь идет о передаче параметров, а не о членских данных. «Ссылки обычно появляются на коже объекта, а указатели - на внутренней».
markh44

Да, я не думаю, что это хороший совет в контексте ссылок на участников.
Тим Ангус

6

В некоторых важных случаях присваиваемость просто не требуется. Часто это легкие обертки алгоритмов, которые упрощают вычисления, не покидая области видимости. Такие объекты являются первыми кандидатами на роль ссылочных членов, поскольку вы можете быть уверены, что они всегда содержат действительную ссылку и никогда не нуждаются в копировании.

В таких случаях не забудьте сделать оператор присваивания (а часто и конструктор копирования) непригодным для использования (путем наследования boost::noncopyableили объявления их закрытыми).

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


5

Поскольку все, кажется, раздают общие правила, я предлагаю два:

  • Никогда и никогда не используйте ссылки в качестве членов класса. Я никогда не делал этого в своем собственном коде (кроме как для того, чтобы доказать себе, что я был прав в этом правиле), и не могу представить себе случай, когда я бы сделал это. Семантика слишком запутанная, и на самом деле ссылки были созданы не для этого.

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

Эти простые правила сослужили мне хорошую службу. Я оставляю правила использования умных указателей (но, пожалуйста, не auto_ptr) в качестве членов класса другим.


2
Не совсем согласен с первым, но разногласия не являются проблемой для оценки обоснования. +1
Дэвид Родригес - дрибес

(Хотя мы, похоже, проигрываем этот спор с хорошими избирателями SO)
Джеймс Хопкин,

2
Я проголосовал против, потому что «Никогда, никогда не используйте ссылки в качестве членов класса», и следующее оправдание мне не подходит. Как объясняется в моем ответе (и комментариях к верхнему ответу) и по моему собственному опыту, бывают случаи, когда это просто хорошо. Я думал так же, как и вы, пока меня не наняли в компанию, где они использовали это хорошо, и это дало мне понять, что есть случаи, когда это помогает. На самом деле я думаю, что настоящая проблема больше в синтаксисе и скрытом подтексте, который делает использование ссылок, поскольку члены требуют от программиста понимания того, что он делает.
Klaim

1
Neil> Я согласен, но это не «мнение» заставило меня проголосовать против, а утверждение «никогда». Поскольку споры здесь в любом случае не кажутся полезными, я отменю свой голос против.
Klaim

2
Этот ответ можно улучшить, объяснив, что сбивает с толку семантика ссылочных членов. Это обоснование важно, потому что на первый взгляд указатели кажутся более семантически неоднозначными (они могут не быть инициализированы, и они ничего не говорят вам о владении базовым объектом).
weberc2

4

Да для: Верно ли утверждение, что объект, содержащий ссылку, не должен быть назначен, поскольку ссылка не должна изменяться после инициализации?

Мои практические правила для членов данных:

  • никогда не используйте ссылку, потому что это предотвращает присвоение
  • если ваш класс отвечает за удаление, используйте boost scoped_ptr (что безопаснее, чем auto_ptr)
  • в противном случае используйте указатель или константный указатель

2
Я даже не могу назвать случай, когда мне понадобится назначение бизнес-объекта.
Николай Голубев

1
+1: Я бы просто добавил, чтобы быть осторожным с членами auto_ptr - любой класс, в котором они есть, должен иметь явно определенную конструкцию присваивания и копирования (сгенерированные вряд ли будут тем, что требуется)
Джеймс Хопкин

auto_ptr также предотвращает (разумное) назначение.

2
@Mykola: Я также убедился, что 98% моих бизнес-объектов не нуждаются в назначении или копировании. Я предотвращаю это с помощью небольшого макроса, который я добавляю в заголовки этих классов, который делает копии c'tors и operator = () закрытыми без реализации. Так что в 98% объектов я также использую ссылки, и это безопасно.
мммммммм

1
Ссылки в качестве членов могут использоваться для явного указания, что время жизни экземпляра класса напрямую зависит от времени жизни экземпляров других классов (или того же самого). «Никогда не используйте ссылку, потому что это предотвращает присваивание» предполагает, что все классы должны разрешать присваивание. Это похоже на предположение, что все классы должны разрешать операцию копирования ...
Клаим

2

Обычно я бы использовал указатель в данных члена только тогда, когда указатель мог быть нулевым или мог измениться. Есть ли другие причины предпочесть указатели ссылкам на элементы данных?

Да. Читаемость вашего кода. Указатель делает более очевидным, что член является ссылкой (по иронии судьбы :)), а не содержащимся в нем объектом, потому что, когда вы его используете, вам нужно отменить ссылку на него. Я знаю, что некоторые люди думают, что это старомодно, но я все же считаю, что это просто предотвращает путаницу и ошибки.


1
Лакос также аргументирует это в своей статье «Крупномасштабная разработка программного обеспечения на C ++».
Йохан Котлински

Я считаю, что Code Complete тоже поддерживает это, и я не против. Хотя это не слишком убедительно ...
markh44

2

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


0

Если я правильно понял вопрос ...

Ссылка как параметр функции вместо указателя: как вы указали, указатель не дает понять, кому принадлежит очистка / инициализация указателя. Предпочитайте общую точку, когда вам нужен указатель, он является частью C ++ 11, и weak_ptr, когда достоверность данных не гарантируется в течение всего времени существования класса, принимающего указанные данные .; также является частью C ++ 11. Использование ссылки в качестве параметра функции гарантирует, что ссылка не равна нулю. Чтобы обойти это, вам нужно изменить языковые функции, а нас не волнуют свободные кодеры.

Ссылка на переменную-член: как указано выше в отношении достоверности данных. Это гарантирует, что указанные данные, на которые затем делается ссылка, действительны.

Перенос ответственности за достоверность переменных на более раннюю точку кода не только очищает более поздний код (класс A в вашем примере), но также делает его понятным для человека, использующего.

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

И на всякий случай (что маловероятно, поскольку это не имеет никакого смысла в контексте вашего кода), другая альтернатива, использовать параметр A без ссылки, без указателя, скопирует A и сделает парадигму бесполезной - I на самом деле не думаю, что вы имеете в виду это как альтернативу.

Кроме того, это также гарантирует, что вы не сможете изменить данные, на которые ссылаются, где можно изменить указатель. Константный указатель будет работать, только если ссылка / указатель на данные не изменяемы.

Указатели полезны, если параметр A для B не был гарантированно установлен или мог быть переназначен. А иногда слабый указатель слишком подробен для реализации, и многие люди либо не знают, что такое weak_ptr, либо просто не любят его.

Разорви, пожалуйста, этот ответ :)

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.