13

Sending the same pointer into two different shared_ptr's is bad, it causes double deallocation, like thus:

int* p = new int;
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p); // BAD

You can accomplish the same purpose with std::enable_shared_from_this:

class Good: public std::enable_shared_from_this<Good>
{
public:
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Good> gp1(new Good);
    std::shared_ptr<Good> gp2 = gp1->getptr();
}

But that still doesn't protect against:

class Good: public std::enable_shared_from_this<Good>
{
public:
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};

int main()
{
    Good* p = new Good;
    std::shared_ptr<Good> gp3(p);
    std::shared_ptr<Good> gp4(p); // BAD
}

which could become a problem if you have code like this:

void Function(std::shared_ptr<Good> p)
{
    std::cout << p.use_count() << '\n';
}

int main()
{
    Good* p = new Good;
    std::shared_ptr<Good> p1(p);
    Function(p);    // BAD
}

Why would I use a regular pointer when there's smart pointers? Because in performance critical code (or for convenience) the overhead of shared_ptr or weak_ptr is undesirable.

To prevent this mistake, I've done:

class CResource : public shared_ptr<Good>
{
public:
    CResource()
    {
    }

    CResource(std::shared_ptr<CBaseControl> c)
        : CResource(c)
    {
    }

private:
    CResource(CBaseControl* p)
    {
    }
};

void Function(CResource p)
{
    std::cout << p.use_count() << '\n';
}

int main()
{
    Good* p = new Good;
    CResource p1(std::shared_ptr<Good>(p));
    Function(p);    // Error
}

This would cause the compiler to error if anyone tries to call Function with a pointer instead of a shared_ptr. It doesn't prevent someone from declaring void Function(std::shared_ptr p) though, but I think that's unlikely.

Is this still bad? Is there a better way of doing this?

4

2 回答 2

29

The solution is simple: never have a raw pointer own memory in the first place. This pattern:

int* p = new int;
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p); // BAD

Simply shouldn’t exist. Eradicate it from your code base. The only places where new is legitimate in C++11 is as an argument to a constructor call to a smart pointer (or in very low level stuff).

I.e. have code like this:

std::shared_ptr<int> p1(new int);

Or better yet (no naked new involved any longer):

auto p1 = std::make_shared<int>();

Note that it’s fine to use raw pointers in code (but I’d question even that in most C++ code I’ve seen). But if you use raw pointers, don’t let them own the memory. Either point to automatic storage (no resource management necessary) or use a unique_ptr and access the raw pointer via its get member function.

于 2012-06-27T10:17:00.843 回答
10

This example doesn't even compile:

void Function(std::shared_ptr<Good> p)
{
    std::cout << p.use_count() << '\n';
}

int main()
{
    Good* p = new Good;
    std::shared_ptr<Good> p1(p);
    Function(p);    // BAD
}

shared_ptr has an explicit constructor precisely to stop that happening.

To make that compile you need to write:

    Function( std::shared_ptr<Good>(p) );

which is pretty obviously wrong, and if someone's going to make that mistake they're just as likely to do:

    Function( CResource(std::shared_ptr<Good>(p)) );

So why have you bothered to write CResource? What does it add?

To expand on Konrad Rudolph's excellent answer:

The answer to your question of how to avoid problems is to follow the RAII idiom, but follow it fully.

We'll ignore the example that doesn't even compile and look at the one above it:

Good* p = new Good;
std::shared_ptr<Good> gp3(p);
std::shared_ptr<Good> gp4(p); // BAD

That code fails to follow the RAII idiom. You acquire a resource:

    Good* p = new Good;

But do not initialize an RAII type. BAD.

Then you initialize an object with some existing resource:

    std::shared_ptr<Good> gp3(p);

This is also BAD. You should initialize an RAII type at the same time as acquiring the resource, not separately (not even only separated by one line.)

Then you repeat the same error:

    std::shared_ptr<Good> gp4(p); // BAD

You've marked this line as "BAD" but actually the previous two lines were just as bad. The third line will cause undefined behaviour, but the first two lines allowed that error to creep in, when it should have been made more difficult. If you never had a Good* hanging around then you couldn't have used it to initialize gp4, you'd have needed to say shared_ptr<Good> gp4(gp3.get()) which is so obviously wrong noone will do it.

The rule is pretty simple: Don't take a raw pointer and put it in a shared_ptr. A raw pointer you didn't allocate is not resource acquisition so don't use it for initialization. The same applies inside Function, it should not take the raw pointer and use it to initialize a shared_ptr that takes ownership of the type.

This is C++, so it's not possible to protect against all the wrong ways to write code, you can't prevent sufficiently motivated idiots from shooting themselves in the foot, but all the guidelines and tools are there to prevent it, if you follow the guidelines.

Whn you have an interface that forces you to transfer ownership by passing a raw pointer, try to replace the interface so it uses a unique_ptr for ownership transfer, or if it's totally impossible to do that then try to wrap the interface in a safer version using unique_ptr for ownership transfer, and only as a last resort, use the dangerous interface but document it very explicitly that ownership is transferred.

于 2012-06-27T11:37:40.403 回答