什么时候应该使用原始指针而不是智能指针?

在阅读了 这个答案之后,尽可能多地使用 聪明的指点,并将“正常”/原始指针的使用减少到最少,这似乎是一种最佳实践。

是真的吗?

43582 次浏览

不,这不是真的。如果一个函数需要一个指针,并且与所有权无关,那么我坚信应该传递一个正则指针,原因如下:

  • 没有所有权,因此不知道要传递哪种智能指针
  • 如果您传递一个特定的指针,比如 shared_ptr,那么您将无法传递(比如说) scoped_ptr

规则是这样的——如果您知道一个实体必须获得对象的某种所有权,那么 总是使用智能指针——它可以提供您所需要的那种所有权。如果没有所有权的概念,永远不要使用智能指针。

例子一:

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();
}

例子二:

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


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

在少数情况下,您可能需要使用指针:

  • 函数指针(显然没有智能指针)
  • 定义自己的智能指针或容器
  • 处理低级编程,其中原始指针是至关重要的
  • 从原始数组衰减

引用计数(特别是 share _ ptr 使用的引用计数)将会中断的一个实例是,当您在指针之外创建一个循环时(例如。A 点指向 B,B 点指向 A,或者 A-> B-> C-> A,或者其他)。在这种情况下,不会自动释放任何对象,因为它们都保持彼此的引用计数大于零。

因此,每当我创建具有父子关系的对象(例如对象树)时,我都会在父对象中使用 share _ ptrs 来保存它们的子对象,但是如果子对象需要一个指向其父对象的指针,我就会使用一个普通的 C/C + + 指针。

始终建议使用智能指针,因为它们清楚地记录了所有权。

然而,我们真正错过的是一个“空白”智能指针,它并不意味着任何所有权的概念。

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>

实际上,这是可能存在的任何智能指针的最简单版本: 记录它不拥有它所指向的资源的类型。

使用智能指针来管理所有权是正确的做法。 相反,在所有权为 没有的地方使用原始指针是一个问题,而 没有是错误的。

下面是一些完全合理的原始指针使用方法(请记住,总是假设它们不属于所有者) :

和推荐信竞争

  • 参数传递; 但是引用不能为 null,因此更可取
  • 作为类成员来表示关联而不是合成; 通常优于引用,因为赋值的语义更直观,此外构造函数设置的不变量可以确保它们在对象的生命周期中不是 0
  • 作为在其他地方拥有的(可能是多态的)对象的句柄; 引用不能为 null,因此它们更可取
  • 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's 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;
};

我认为这里给出了一个比较全面的答案: 我什么时候使用哪种指针?

摘自这个链接: “使用哑指针(原始指针)或 非拥有参考文献对资源的引用,当您知道 资源会存活下来是引用对象/范围时。”(粗体保存自原件)

问题是,如果您编写的代码是通用的,那么很难绝对确定对象会比原始指针存在时间长。考虑一下这个例子:

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,并且在默认情况下,如果您试图访问已经被删除的对象,它们会抛出异常。

还要注意的是,注册指针可以通过编译时指令“禁用”(自动替换为原始指针) ,允许仅在调试/测试/测试模式下使用(并产生开销)。因此,您实际上应该很少使用实际的原始指针。

没错,我看不出原始指针比智能指针有什么好处,尤其是在复杂的项目中。

不过,对于临时和轻量级的使用,原始指针是可以的。

我相信智能指针应该尽可能多地使用,即使在原始指针已经足够的情况下。unique_ptr可以帮助管理资源生命周期,同时仍然保持小型和快速。别回头!