Как пройти std:: unique_ptr?
У меня есть первая попытка использовать С++ 11 unique_ptr
; Я заменяю полиморфный необработанный указатель внутри моего проекта, который принадлежит одному классу, но довольно часто проходит.
У меня были такие функции, как:
bool func(BaseClass* ptr, int other_arg) {
bool val;
// plain ordinary function that does something...
return val;
}
Но вскоре я понял, что не могу переключиться на:
bool func(std::unique_ptr<BaseClass> ptr, int other_arg);
Потому что вызывающий должен обработать свойство указателя на функцию, чего я не хочу. Итак, каково наилучшее решение моей проблемы?
Я, хотя передаю указатель как ссылку, вот так:
bool func(const std::unique_ptr<BaseClass>& ptr, int other_arg);
Но я чувствую себя очень неудобно в этом, во-первых, потому что кажется неинтуитивным передавать что-то уже набранное как _ptr
как ссылку, что будет ссылкой на ссылку. Во-вторых, потому что сигнатура функции становится еще больше. В-третьих, потому что в сгенерированном коде для достижения моей переменной потребуется два последовательных указателя указателя.
Ответы
Ответ 1
Если вы хотите, чтобы функция использовала pointee, передайте ссылку на нее. Нет никакой причины привязать функцию к работе только с каким-то умным указателем:
bool func(BaseClass& base, int other_arg);
И на сайте вызова используйте operator*
:
func(*some_unique_ptr, 42);
В качестве альтернативы, если аргументу base
разрешено быть нулевым, сохраняйте подпись как есть и используйте функцию члена get()
:
bool func(BaseClass* base, int other_arg);
func(some_unique_ptr.get(), 42);
Ответ 2
Преимущество использования std::unique_ptr<T>
(кроме того, что не нужно запоминать вызовы delete
или delete[]
явно) заключается в том, что он гарантирует, что указатель либо nullptr
, либо указывает на действительный экземпляр ( базовый). Я вернусь к этому после ответа на ваш вопрос, но первое сообщение DO использует интеллектуальные указатели для управления временем жизни динамически выделенных объектов.
Теперь ваша проблема заключается в том, как использовать это со старым кодом.
Мое предложение состоит в том, что если вы не хотите передавать или передавать права собственности, вы должны всегда передавать ссылки на объект. Объявите свою функцию следующим образом (с или без const
квалификаторов, если необходимо):
bool func(BaseClass& ref, int other_arg) { ... }
Затем вызывающий, который имеет std::shared_ptr<BaseClass> ptr
, либо обрабатывает случай nullptr
, либо запрашивает bool func(...)
для вычисления результата:
if (ptr) {
result = func(*ptr, some_int);
} else {
/* the object was, for some reason, either not created or destroyed */
}
Это означает, что любой вызывающий должен обещать, что ссылка действительна и что она будет оставаться действительной во время выполнения тела функции.
Вот почему я твердо верю, что вы должны не передавать исходные указатели или ссылки на интеллектуальные указатели.
Необработанный указатель - это только адрес памяти. Может иметь одно из (по крайней мере) 4 значений:
- Адрес блока памяти, в котором находится желаемый объект. (хороший)
- Адрес 0x0, который вы можете быть уверенным, не является неразрешимым и может иметь семантику "ничего" или "нет объекта". (плохой)
- Адрес блока памяти, который находится за пределами адресного пространства вашего процесса (разыменование его, мы надеемся, приведет к сбою вашей программы). (уродливый)
- Адрес блока памяти, который может быть разыменован, но который не содержит того, что вы ожидаете. Возможно, указатель был случайно изменен, и теперь он указывает на другой доступный для записи адрес (полностью другой переменной в вашем процессе). Запись в это место памяти вызовет массу удовольствия, иногда, во время исполнения, потому что ОС не будет жаловаться, пока вам разрешено писать там. (Zoinks!)
Правильное использование интеллектуальных указателей облегчает довольно страшные случаи 3 и 4, которые обычно не обнаруживаются во время компиляции и которые вы обычно испытываете только во время выполнения, когда ваша программа вылетает или неожиданно происходит.
Передача интеллектуальных указателей в качестве аргументов имеет два недостатка: вы не можете изменить const
-ность заостренного объекта без создания копии (что добавляет служебные данные для shared_ptr
и не возможно для unique_ptr
), и вы все еще осталось со вторым значением (nullptr
).
Я отметил второй случай как (плохой) с точки зрения дизайна. Это более тонкий аргумент в отношении ответственности.
Представьте, что это означает, когда функция получает nullptr
в качестве своего параметра. Сначала нужно решить, что с ним делать: использовать "магическое" значение вместо недостающего объекта? полностью изменить поведение и вычислить что-то еще (что не требует объекта)? паника и исключение? Более того, что происходит, когда функция принимает 2, или 3 или даже больше аргументов с помощью необработанного указателя? Он должен проверять каждый из них и соответствующим образом адаптировать свое поведение. Это добавляет совершенно новый уровень поверх проверки ввода без какой-либо реальной причины.
Вызывающий должен быть тем, у кого достаточно контекстной информации, чтобы принимать эти решения, или, другими словами, плохое становится менее пугающим, чем больше вы знаете. С другой стороны, функция должна просто убедить звонящего, что память, на которую она указана, безопасна для работы в соответствии с назначением. (Ссылки по-прежнему являются адресами памяти, но концептуально представляют собой обещание действительности.)
Ответ 3
Я согласен с Martinho, но я думаю, что важно указать семантику собственности pass-by-reference. Я думаю, что правильное решение состоит в том, чтобы использовать простую ссылку на ссылку здесь:
bool func(BaseClass& base, int other_arg);
Общепринятое значение pass-by-reference в С++ похоже на то, что вызывающая функция функции сообщает функции "здесь, вы можете одолжить этот объект, использовать его и изменить его (если не const), но только на время функционирования тела функции". Это никоим образом не противоречит правилам владения unique_ptr
, потому что объект просто заимствуется на короткий период времени, фактическая передача собственности не происходит (если вы одалживаете свою машину кому-то, подписать титул ему?).
Итак, хотя может показаться плохим (конструктивно, методы кодирования и т.д.) вытащить ссылку (или даже необработанный указатель) из unique_ptr
, на самом деле это не потому, что она полностью соответствует с правилами собственности, установленными unique_ptr
. И тогда, конечно, есть и другие приятные преимущества, такие как чистый синтаксис, отсутствие ограничений только на объекты, принадлежащие unique_ptr
, и так далее.
Ответ 4
Лично я избегаю ссылки на указатель/умный указатель. Потому что что происходит, если указатель nullptr
? Если вы измените подпись на это:
bool func(BaseClass& base, int other_arg);
Возможно, вам придется защитить свой код от нулевых указаний указателя:
if (the_unique_ptr)
func(*the_unique_ptr, 10);
Если класс является единственным владельцем указателя, второй вариант Martinho выглядит более разумным:
func(the_unique_ptr.get(), 10);
В качестве альтернативы вы можете использовать std::shared_ptr
. Однако, если для delete
есть один объект, ответственный за delete
, служебные данные std::shared_ptr
не окупаются.