HNJSlater
HNJSlater

Reputation: 293

Enforce no bald pointers in C++

Generally speaking, in this crazy modern world full of smart pointers, I'm coming round to the fact that bald/bare pointers shouldn't be in method signatures.

I suppose there might be some cases where you need bald pointers for performance reasons but certainly I've always managed with references or references to smart pointers.

My question is this, can anyone suggest an automated way to enforce this? I can't see a way with clang, GCC or vera++

Upvotes: 4

Views: 540

Answers (4)

Chris Drew
Chris Drew

Reputation: 15334

I think it is ok for non-owning raw-pointers to be used in method signatures. It is not ok to use "owning" raw-pointers.

For example this is quite ok:

void bar(const Widget* widget) {
  if (widget)
    widget->doSomething();
}

void foo() {
  Widget w;
  bar(&w);
}

If you want the Widget to be nullable you can't use a reference and using a smart pointer would be slower and not express the ownership model. The Widget can be guaranteed to be alive for the whole scope of foo. bar simply wants to "observe" it so a smart pointer is not needed. Another example where a non-owning raw-pointer is appropriate is something like:

class Node {
 private:
  std::vector<std::unique_ptr<Node>> children_;  // Unique ownership of children
  Node* parent_;
 public:
  // ...
};

The Node "owns" its children and when a Node dies its children die with it. Providing encapsulation of Node hasn't been broken, a Node can guarantee its parent is alive (if it has one). So a non-owning raw-pointer is appropriate for a back reference to the parent. Using smart pointers where they are not required makes code harder to reason about, can result in memory leaks and is premature pessimization.

If smart pointers are used throughout instead, all kinds of things can go wrong:

// Bad!
class Node {
 private:
  std::vector<std::shared_ptr<Node>> children_; // Can't have unique ownership anymore
  std::shared_ptr<Node> parent_; // Cyclic reference!
 public:

  // Allow someone to take ownership of a child from its parent.
  std::shared_ptr<Node> getChild(size_t i) { return children_.at(i); }
  // ...
};

Some of these problems can be solved by using weak_ptr instead of shared_ptr but they still need to be used with care. A weak_ptr is a pointer that has the option of owning.

The point is to use the appropriate kind of pointer in the appropriate context and to have an ownership model that is as simple as possible.

And no, I don't think there is an automated way of enforcing this, it is part of the design. There are tricks you can use like Cheersandhth.-Alf said to try and enforce a particular class is used in the way you want by restricting access.

Upvotes: 1

MSalters
MSalters

Reputation: 179809

The problem with this idea is that in the smart pointer world, T* is just a shorthand for optional<T&>. It no longer implies memory management, and as a result it becomes a safe idiom to identify an optional non-owned parameter.

Upvotes: 0

Cheers and hth. - Alf
Cheers and hth. - Alf

Reputation: 145269

I don't think it's a good idea to completely stay away from raw (bald, bare) pointers even for function arguments. But it can sometimes be a good idea to make sure that all instances of a certain type are dynamic and owned by smart pointers. This involves two measures:

  • Ensure (to the degree practical) that the type can only be instantiated dynamically.

  • Ensure (to the degree practical) that instantiation yields a smart pointer.

The first point is easy: just make the destructor non-public, and voilá, the few remaining ways to instantiate that type non-dynamically are not very natural. Any simple attempt to declare a local or namespace scope variable will fail. As will simple attempts to use that type directly as a data member type:

#include <memory>

template< class Type >
void destroy( Type* p ) { delete p; }

class Dynamic
{
template< class Type > friend void destroy( Type* );
protected:
    virtual ~Dynamic() {}
};

//Dynamic x;  //! Nope.

struct Blah
{
    Dynamic x;      //! Nope, sort of.
};

auto main()
    -> int
{
    //Dynamic x;        //! Nope.
    new Blah;           //!Can't delete, but new is OK with MSVC 12...
}

Visual C++ 12.0 unfortunately accepts the above code as long as the out-commented statements remain out-commented. But it does issue a stern warning about its inability to generate a destructor.

So, Dynamic can only be created dynamically (without using low-level shenanigans, or ignoring that MSVC warning and accepting a memory leak), but how to ensure that every new instance is owned by a smart pointer?

Well you can restrict access to the constructors so that a factory function must be used. Another way to ensure use of the factory function is to define a placement allocation function, operator new, that an ordinary new-expression can't access. Since that's most esoteric and possibly non-trivial, and not of clear-cut value, the below code just restricts access to the constructors:

#include <memory>       // std::shared_ptr
#include <utility>      // std::forward

// A common "static" lifetime manager class that's easy to be-"friend".
struct Object
{
    template< class Type >
    static
    void destroy( Type* p ) { delete p; }

    template< class Type, class... Args>
    static
    auto create_shared( Args&&... args )
        -> std::shared_ptr<Type>
    {
        return std::shared_ptr<Type>(
            new Type( std::forward<Args>( args )... ),
            &destroy<Type>
            );
    }
};

class Smart
{
friend class Object;
protected:
    virtual ~Smart() {}
    Smart( int ) {}
};

//Smart x;  //! Nope.

struct Blah
{
    //Smart x;      //! Nope, sort of.
};

auto main()
    -> int
{
    //Smart x;        //! Nope.
    //new Blah;       //!Can't delete.
    //new Smart( 42 );       // Can't new.
    Object::create_shared<Smart>( 42 );      // OK, std::shared_ptr
}

One problem with this approach is that the standard library's make_shared doesn't support a custom deleter, and shared_ptr doesn't use the common deleter of unique_ptr. One may hope that perhaps this will be rectified in future standard.


Disclaimer: I just wrote this for this answer, it's not been extensively tested. But I did this in old times with C++98, then with a macro for the create-functionality. So the principle is known to be sound.

Upvotes: 0

Phil Miller
Phil Miller

Reputation: 38118

I don't know of any automated tool to do this, but such a check wouldn't be too hard to implement in cppcheck, which is open source and AIUI offers an easy way to add new rules.

Upvotes: 0

Related Questions