看完这个答案后,看起来尽可能多地使用智能指针是一个最佳实践,并将"正常"/raw指针的使用减少到最低限度。
是真的吗?
不,这不是真的。如果一个函数需要一个指针,而与所有权无关,那么我强烈认为应该传递一个常规指针,原因如下:
- 没有所有权,因此你不知道传递什么样的智能指针
- 如果你传递一个特定的指针,比如
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();
}
Example2:
Object* createObject() //bad
{
return new Object;
}
some_smart_ptr<Object> createObject() //good
{
return some_smart_ptr<Object>(new Object);
}
使用智能指针来管理所有权是正确的事情。相反,在所有权为而不是的地方使用原始指针是错误的,而不是。
下面是一些完全合法的原始指针用法(记住,总是假设它们是非拥有的):
与引用
竞争- 参数传递;但引用不能为空,所以首选
- 作为类成员来表示关联而不是组合;通常比引用更可取,因为赋值的语义更直接,此外,构造函数设置的不变量可以确保它们在对象的生命周期内不是
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'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;
};
引用计数(特别是shared_ptr使用)将崩溃的一个实例是当您创建指针的循环(例如a指向B, B指向a,或a ->B->C-> a,等等)。在这种情况下,任何对象都不会被自动释放,因为它们都保持彼此的引用计数大于零。
因此,每当我创建具有父子关系的对象(例如对象树)时,我将在父对象中使用shared_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>
这实际上是可能存在的智能指针的最简单版本:一种记录它不拥有它所指向的资源的类型。
在少数情况下,您可能需要使用指针:
- 函数指针(显然没有智能指针)
- 定义自己的智能指针或容器
- 处理低级编程,其中原始指针至关重要
- 从原始数组衰减
我认为这里给出了更彻底的答案:我何时使用哪种指针?
摘自那个链接:"使用哑指针(原始指针)或引用对资源的非所有权引用,并且当您知道资源将比引用对象/作用域活得更长时。"(粗体从原文保留)
问题是,如果你写的代码是通用的,它并不总是容易绝对确定对象将比原始指针的寿命长。考虑这个例子:
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
,并且在默认情况下,如果您试图访问已被删除的对象,将抛出异常。
还要注意,注册的指针可以用编译时指令"禁用"(自动替换为原始指针),允许它们仅在调试/测试/beta模式下使用(并产生开销)。所以你应该很少使用原始指针。
这是真的。我看不出原始指针比智能指针有什么好处,尤其是在复杂的项目中。
对于临时和轻量级的使用,原始指针是可以的。