Boost logo

Boost :

From: David B. Held (dheld_at_[hidden])
Date: 2002-10-02 01:35:02


It seems to me that smart pointers present a bit of a problem when it
comes time to pass them as arguments to a function. Consider a
case with raw pointers:

class foo
{
public:
    int bar() const { return bar_; }
    void baz(int n) { bar_ = n; }
private:
    int bar_;
};

void g(foo const* p)
{
    p->bar();
    p->baz(3); // Error!
}

We can, of course, call foo::bar(), because it is a const function. We
cannot call foo::baz(), because it is not const. Now, consider the case
with, say, shared_ptr<>:

typedef shared_ptr<foo> foo_ptr;

void g(foo_ptr const& p)
{
    p->bar();
    p->baz(3); // No Error!
}

Again, it is perfectly safe to call foo::bar(). However, we have lost the
ability to guard against calling foo::baz(), because
shared_ptr<>::operator->() always returns T*. The correct solution, of
course, is the following:

void g(shared_ptr<foo const> p)
{
    p->bar();
    p->baz(3); // Error!
}

Now, once again, we are prevented from calling foo::baz(), because
operator->() returns foo const*. However, there are a few undesirable
features of this setup. First, we can't declare the proper type from our
typedef foo_ptr. So we have to refer to the original type, which binds
us to shared_ptr explicitly (prevents us from swapping it out with a
different pointer). Second, the syntax is rather non-obvious, especially
considering how often people ask about shared_ptr's const-correctness.
And third, I didn't bother making the second version a const&, because
it has to make a copy of the pointer anyway, since they are different
types.

Now, to overcome this situation, I considered a type adaptor like so:

template <typename T>
struct param
{ typedef shared_ptr<typename T::element_type const> const& type; }

which one would use like so:

void g(param<foo_ptr>::type p)
{
    p->bar();
    p->baz(3); // Error!
}

However, this solution obviously suffers from a few idiosyncracies as
well. First, this syntax isn't much more obvious than the simple correct
syntax. Second, you still end up with copying. What would be nice is
a solution that did not require syntactic acrobatics and avoided any
needless copies. So I would like to hear opinions on this proposal:

Let operator->() const and operator*() const return T const* and
T const&, respectively, and let operator->() and operator*() return
T* and T&.

Even though this does not implement technically correct semantics, I
believe that it fulfills the practical semantics of most usage. That is, it
seems extremely unlikely that shared_ptr<T> const is stored as data by
very many people. It seems that the most likely appearance of
shared_ptr<T> const is in a function signature, where the user probably
also would like T to be const. My proposal is technically incorrect in
that mutable T disallows shared_ptr<T> const& as a function argument,
even though it should be technically legal. This forces shared_ptr<T>
or shared_ptr<T>& as the argument, which would allow the user to
modify the pointer itself, when the desired effect is simply to modify
T. However, in my experience, mutating operations on smart pointers
themselves are fairly rare, especially in comparison to dereferencing
or calling through the pointer. So I think the greater good is in
convenient const correctness of T rather than technical const
correctness of shared_ptr<T>.

That is, with my proposal, I believe that the following would work
correctly, and with negligible side effects:

void g(foo_ptr const& p)
{
    p->bar();
    p->baz(3); // Error! (Correct)
}

Dave


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk