Search code examples
c++nullsmart-pointersdefault-constructor

Default Initialize or Check For Null


I'd like to know is it better to specify a default initialization for a smart-pointer or do a NULL value check before accessing the smart-pointers methods?

Currently I've been using the method below to avoid calling increment() on a NULL pointer. Is this a reasonable way of doing things or is there a pitfall that I don't see?

Note: We use a custom smart-pointer class and I don't have the Boost libraries on my current configuration to test compile this code. This should compile, but YMMV.

Example.h

#include <boost/shared_ptr.hpp>

class Foo
{
public:
  Foo() : mFoo(0) {}
  Foo(int rawValue) : mFoo(rawValue) {}

  void  increment() { mFoo++; }

private:
  int mFoo;
};

typedef boost::shared_ptr<Foo> FooSP;

class MyClass
{
public:
  MyClass() : mFoo(new Foo()) {}

  FooSP       foo() { return mFoo; }
  void        setFoo(FooSP newFoo) { mFoo = newFoo; }

private:
  FooSP mFoo;
};

Main.cpp

#include <Example.h>

int main()
{
  MyClass temp;                    // Default-constructed
  temp.foo()->increment();         // Increment Foo's member integer
                                   // Before: mFoo = 0
                                   // After:  mFoo = 1
  FooSP tempFoo = new Foo(10);     // Create a Foo with a default size
  temp.setFoo(FooSP(new Foo(10))); // Explicitly set the FooSP member
  temp.foo()->increment();         // Increment the new FooSP
                                   // Before: mFoo = 10
                                   // After:  mFoo = 11
  return 0;
}

Solution

  • is it better to specify a default initialization for a smart-pointer or do a NULL value check before accessing the smart-pointers methods?

    There is no right answer which applies to every case (more soon). If I had to err to one or the other, I would err toward NULL testing without default initialization because that's an obvious programmer error which can be detected and corrected easily.

    However, I think the right answer is that there are good reasons we use multiple idioms for construction and initialization, and that you should choose the best approach for your program.

    Typically, I will be explicit (no default or no default initialization) in the lower level classes, as well as complex higher level classes. When the classes are mid-level and defaults and ownership are more obvious (often because of limited use cases), then a default may be sensible.

    Often, you will just want to be consistent, to avoid surprising clients. You'll also need to be aware of the complexity of allocating default-initialized objects. If it's big and complex to create, and a default does not make sense, then you are simply wasting a lot of resources when the default-constructed object is the wrong choice.

    • a) do not apply a default where it does not make sense. the default should be obvious.
    • b) avoid wasted allocations.

    In addition to the approaches you have mentioned, there are a few other angles you might also consider:

    • Matching Foo's declared constructors in MyClass. At least, the ones which pertain to MyClass.
    • If copyable and efficient to copy, passing a Foo to MyClass's constructor.
    • Passing Foo in a container (smart pointer in this case) to MyClass's constructor to remove any ambiguity and to offer the client the option to construct (and share, in the case of a shared pointer) Foo as they desire.

    Is this a reasonable way of doing things or is there a pitfall that I don't see?

    Wasted allocations. Surprising results. It can restrict capabilities. The most obvious, broadly applicable problems are time and resource consumption.

    To illustrate some scenarios:

    • say Foo reads a 1MB file every time it is constructed. when construction parameters are necessary and the default is not the right option, the file would have to be read a second time. the innocent default would double the disk io required.
    • in another case, an omitted construction parameter may be another large or complex shared pointer. if absent, Foo may create its own -- when the resource could/should have been shared.

    Constructors parameters are often very important, and often should not be erased from the interface. It's certainly fine to do so in some cases, but these conveniences can introduce a lot of restrictions or introduce much unnecessary allocations and CPU time as the contained object's complexity increases.

    Using both approaches in your programs is fine. Using additional approaches I outlined is also fine. Specifically, using the right approach for the problem is ideal - there are multiple ways to implement ideal solutions available; you just have to determine what that is in the context of what it is your program is trying to do. All these approaches have separate pros and cons - there is often an ideal match for the context of your program's operation and exposed interfaces.