Smart Pointer Parameters
Problem #1
下面的函数声明有哪些性能问题
void f( shared_ptr<widget> w);
在回答这个问题前,我们先搞清楚shared_ptr
是如何实现的
struct shared_ptr_control_block {
some_atomic_counter_type reference_count;
// maybe other stuff....
};
template<typename T>
struct shared_ptr {
T *data;
shared_ptr_control_block *cntrl;
};
当一个shared_ptr
被创建时,这个shared_ptr_control_block
是会在heap上单独创建。shared_ptr
的构造函数,copy构造和析构函数都需要维护这个refcount。为了在多线程的环境下能正确工作,refcount的bump是atomic的。和正常的increament, decrement相比,atomic的操作是非常expensive的,uops.info的数据显示atomic increment的速度是non-atomic increment的5倍。如果多个线程同时bump refcount,性能的损失会非常大,因为原子操作无法在多线程中同时执行。
回到上面的问题, w
作为参数会被触发shared_ptr
的copy构造函数,进而会increment其refcount。当函数结束的时候,w
会析构,进而decrement refcount。在多线程环境下,如果f
调用频繁,refcount的bump会非常影响性能。
因此,shared_ptr
作为参数传递要谨慎,我们需要尽可能少的copy它。有些时候callee只是使用shared_ptr
而不需要take ownership的,因此这个时候可以用const T&
, T&
或者直接用raw pointer。比如下面例子中,f
只是使用Widget,它并不需要接受一个shared_ptr
,传一个引用即可。
void f(const std::shared_ptr<Widget>& w) {
w->foo();
}
void g() {
auto p = std::make_shared<Widget>(/*...*/);
f(p);
}
Problem #2
承接上一个问题,假如我们有一个widget
对象w
,我们要将它传给f
,我们应该如何传递w
,考虑下面几种情况,并解释什么时候该用哪一种,以及什么时候该加上const
void f( Widget* w); (a)
void f( Widget& w); (b)
void f( unique_ptr<Widget> w); (c)
void f( unique_ptr<Widget>& w); (d)
void f( shared_ptr<Widget> w); (e)
void f( shared_ptr<Widget>& w); (f)
(a) and (b): Prefer passing parameters by * or &
对于(a)和(b)本质是一样的,这个前面已经提到过,如果f
不需要take ownership,而只是使用widget
对象,那么传指针或者引用是合理的
void f(Widget* w) {
w->foo();
}
void g() {
auto p = std::make_shared<Widget>(/*...*/);
f(p);
}
但是如果是多线程的情况下,则需要小心w
是否为已经变成dangling pointer。
(c) Passing unique_ptr by value means “sink.”
unique_ptr
是不允许copy的,因此如果直接传递,编译器会报错,只能使用std::move
void f(unique_ptr<Widget> w)) {
w->foo();
}
void g() {
auto p = std::make_unique<Widget>();
f(p); //error
}
void f(unique_ptr<Widget> w)) {
w->foo();
}
void g() {
auto p = std::make_unique<Widget>();
f(std::move(p)); //good
// now p is nullptr
}
实际上函数f
相当于告诉函数g
它需要take ownership,请不要继续使用p
,那么如果按照以前的写法,相当于手动添加下面注释
// Smelly 20th-century alternative
void bad_sink( Widget* p ); // will destroy p; PLEASE READ THIS COMMENT
// Sweet self-documenting self-enforcing modern version (c)
void good_sink( unique_ptr<Widget> p );
Guideline: Express a “sink” function using a by-value unique_ptr parameter.
由于f
会take ownership,这种情况unique_ptr
往往不需要声明const
,加或者不加const
并没有影响,void f(const unique_ptr<Widget> w)
同样也可以compile。
(d) Passing unique_ptr by reference is for in/out unique_ptr parameters.
如果传引用则表示w
是一个in/out参数,f
可以mutate w
。这不是一个很好的做法,因为函数g
无法知道p
的状态,比如下面代码中,f()
release了w
,导致p
变成nullptr
void f(std::unique_ptr<Widget>& w){
w.release();
}
void g() {
std::unique_ptr<Person> p = std::make_unique<Widget>("peter");
f(p);
// p is now nullptr
}
Guideline: Use a non-const unique_ptr& parameter only to modify the unique_ptr.
如果想要限制f
不mutate w
,则可以将w
声明成const std::unique_ptr<Person>& w
。但是如果使用const
引用,则相当于传入了一个Widget*
,又回到了前面(a)
和(b)
的case
Guideline: Don’t use a const unique_ptr& as a parameter; use widget* instead.
(e) Passing shared_ptr by value implies taking shared ownership.
这个case前面讨论过的,f
会触发shared_ptr
的copy构造函数。除非f
明确要share ownership,否则这种传参方式有一定的perf开销。如果f
既想take ownership,而又不想bump up ref count,那么可以用std::move
,但要注意,move后,g
中的p
会变成nullptr
using namespace std;
void f(shared_ptr<Widget> w){
// ref count = 2
}
void g() {
shared_ptr<Widget> p =
make_shared<Widget>();
//ref count = 1
f(p);
// ref count = 1
}
using namespace std;
void f(shared_ptr<Widget> w){
// ref count = 1
}
void g() {
shared_ptr<Widget> p =
make_shared<Widget>();
//ref count = 1
f(std::move(p));
// ref count = 0
//p is nullptr
}
(f) Passing shared_ptr& is useful for in/out shared_ptr manipulation.
(f)的情况和(d)类似,意思w
将作为in/out
参数,g
可以mutate w
,因此这不是一个很安全的做法。如果加上const
,则f
将表达另一个含义
using namespace std;
void f(const shared_ptr<Widget>& w){
// ref count = 1
}
void g() {
shared_ptr<Widget> p = make_shared<Widget>();
//ref count = 1
f(p);
// ref count = 1
//p is still valid
}
此时,f
可以share w
的ownership,但由于我们声明了引用,因此并不会调用shared_ptr
的拷贝构造函数,因此不会有性能的问题,是一种两全其美的方法,因此实践中,要尽可能的使用这种方式。
Guideline: Use a non-const shared_ptr& parameter only to modify the shared_ptr. Use a const shared_ptr& as a parameter only if you’re not sure whether or not you’ll take a copy and share ownership; otherwise use widget* instead (or if not nullable, a widget&).