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&).

Resources