Подтвердить что ты не робот

Когда следует использовать необработанные указатели над интеллектуальными указателями?

Прочитав этот ответ, похоже, что рекомендуется использовать как можно больше умных указателей и свести к минимуму использование "обычных"/необработанных указателей.

Это правда?

4b9b3361

Ответ 1

Нет, это неправда. Если функция нуждается в указателе и не имеет ничего общего с правом собственности, я твердо верю, что регулярный указатель должен быть передан по следующим причинам:

  • Нет собственности, поэтому вы не знаете, какой умный указатель для передачи
  • Если вы передадите определенный указатель, например shared_ptr, то вы не сможете передать, скажем, scoped_ptr

Правило будет таким: если вы знаете, что объект должен обладать определенным видом собственности на объект, всегда использовать интеллектуальные указатели - тот, который дает вам необходимый вид собственности. Если нет понятия собственности, никогда использовать интеллектуальные указатели.

Пример1:

void PrintObject(shared_ptr<const Object> po) //bad
{
    if(po)
      po->Print();
    else
      log_error();
}

void PrintObject(const Object* po) //good
{
    if(po)
      po->Print();
    else
      log_error();
}

Пример 2:

Object* createObject() //bad
{
    return new Object;
}

some_smart_ptr<Object> createObject() //good
{
   return some_smart_ptr<Object>(new Object);
}

Ответ 2

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

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

где они конкурируют со ссылками

  • передача аргументов; но ссылки не могут быть нулевыми, поэтому предпочтительнее
  • как члены класса для обозначения ассоциации, а не композиции; обычно предпочтительнее ссылок, поскольку семантика присваивания более проста, и, кроме того, инвариант, созданный конструкторами, может гарантировать, что они не 0 для времени жизни объекта
  • как дескриптор (возможно, полиморфного) объекта, находящегося где-то в другом месте; ссылки не могут быть нулевыми, поэтому они предпочтительнее
  • std::bind использует соглашение, в котором переданные аргументы копируются в полученный функтор; однако std::bind(&T::some_member, this, ...) делает только копию указателя, тогда как std::bind(&T::some_member, *this, ...) копирует объект; std::bind(&T::some_member, std::ref(*this), ...) является альтернативой

где они не конкурируют со ссылками

  • как итераторы!
  • аргумент необязательных параметров; здесь они конкурируют с boost::optional<T&>
  • как дескриптор (возможно, полиморфного) объекта, находящегося где-то в другом месте, когда они не могут быть объявлены в месте инициализации; снова, конкурируя с boost::optional<T&>

В качестве напоминания почти всегда неправильно писать функцию (которая не является конструктором или членом функции, который, например, принимает право собственности), который принимает интеллектуальный указатель, если он, в свою очередь, не передает его конструктору (например, он правильный для std::async, потому что семантически это близко к вызову конструктора std::thread). Если это синхронно, нет необходимости в умном указателе.


Чтобы воспроизвести здесь фрагмент, демонстрирующий некоторые из перечисленных выше способов. Мы пишем и используем класс, который применяет функтор к каждому элементу std::vector<int> при записи некоторого вывода.

class apply_and_log {
public:
    // C++03 exception: it acceptable to pass by pointer to const
    // to avoid apply_and_log(std::cout, std::vector<int>())
    // notice that our pointer would be left dangling after call to constructor
    // this still adds a requirement on the caller that v != 0 or that we throw on 0
    apply_and_log(std::ostream& os, std::vector<int> const* v)
        : log(&os)
        , data(v)
    {}

    // C++0x alternative
    // also usable for C++03 with requirement on v
    apply_and_log(std::ostream& os, std::vector<int> const& v)
        : log(&os)
        , data(&v)
    {}
    // now apply_and_log(std::cout, std::vector<int> {}) is invalid in C++0x
    // && is also acceptable instead of const&&
    apply_and_log(std::ostream& os, std::vector<int> const&&) = delete;

    // Notice that without effort copy (also move), assignment and destruction
    // are correct.
    // Class invariants: member pointers are never 0.
    // Requirements on construction: the passed stream and vector must outlive *this

    typedef std::function<void(std::vector<int> const&)> callback_type;

    // optional callback
    // alternative: boost::optional<callback_type&>
    void
    do_work(callback_type* callback)
    {
        // for convenience
        auto& v = *data;

        // using raw pointers as iterators
        int* begin = &v[0];
        int* end = begin + v.size();
        // ...

        if(callback) {
            callback(v);
        }
    }

private:
    // association: we use a pointer
    // notice that the type is polymorphic and non-copyable,
    // so composition is not a reasonable option
    std::ostream* log;

    // association: we use a pointer to const
    // contrived example for the constructors
    std::vector<int> const* data;
};

Ответ 3

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

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

template <typename T>
class ptr // thanks to Martinho for the name suggestion :)
{
public:
  ptr(T* p): _p(p) {}
  template <typename U> ptr(U* p): _p(p) {}
  template <typename SP> ptr(SP const& sp): _p(sp.get()) {}

  T& operator*() const { assert(_p); return *_p; }
  T* operator->() const { assert(_p); return _p; }

private:
  T* _p;
}; // class ptr<T>

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

Ответ 4

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

По этой причине, всякий раз, когда я создаю объекты с родительско-дочерними отношениями (например, с деревом объектов), я буду использовать shared_ptrs в родительских объектах для хранения своих дочерних объектов, но если дочерним объектам нужен указатель назад к их родителям, я буду использовать простой указатель C/С++ для этого.

Ответ 5

Несколько случаев, когда вы можете использовать указатели:

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

Ответ 6

Я думаю, что здесь был дан более подробный ответ: Какой тип указателя я использую, когда?

Выдержка из этой ссылки: "Используйте немые указатели (необработанные указатели) или ссылки для ссылок, не относящихся к владельцам, и когда вы знаете, что ресурс перейдет в ссылку объект/область действия". (выделено жирным шрифтом от оригинала)

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

struct employee_t {
    employee_t(const std::string& first_name, const std::string& last_name) : m_first_name(first_name), m_last_name(last_name) {}
    std::string m_first_name;
    std::string m_last_name;
};

void replace_current_employees_with(const employee_t* p_new_employee, std::list<employee_t>& employee_list) {
    employee_list.clear();
    employee_list.push_back(*p_new_employee);
}

void main(int argc, char* argv[]) {
    std::list<employee_t> current_employee_list;
    current_employee_list.push_back(employee_t("John", "Smith"));
    current_employee_list.push_back(employee_t("Julie", "Jones"));
    employee_t* p_person_who_convinces_boss_to_rehire_him = &(current_employee_list.front());

    replace_current_employees_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list);
}

К большому удивлению, функция replace_current_employees_with() может непреднамеренно привести к тому, что один из ее параметров будет освобожден до того, как он закончит использовать его.

Итак, хотя сначала может показаться, что функция replace_current_employees_with() не нуждается в владении этими параметрами, она нуждается в какой-то защите от возможности ее параметров, которая коварно освобождается, прежде чем она закончит их использовать. Самое простое решение состоит в том, чтобы фактически взять (временно разделенное) право собственности на параметр (ы), предположительно через shared_ptr.

Но если вы действительно не хотите брать на себя ответственность, теперь есть безопасный вариант - и это бесстыдная часть части ответа - " зарегистрированных указателей". "зарегистрированные указатели" - это интеллектуальные указатели, которые ведут себя как необработанные указатели, за исключением того, что они (автоматически) установлены на null_ptr, когда целевой объект уничтожается, и по умолчанию генерирует исключение, если вы пытаетесь получить доступ к объекту, который уже был удален.

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

Ответ 7

Это правда. Я не вижу преимуществ сырых указателей над умными указателями, особенно в сложном проекте.

Для временного и легкого использования исходные указатели в порядке.