David Frickert
David Frickert

Reputation: 127

Getter and instance variable, reference, pointer, or object?

Let's say I have a class Position:

class Position{
public:
    Position(int x, int y) : x(x), y(y) {}
    int getX(void) { return x; }
    int getY(void) { return y; }
private:
    int x;
    int y;  
};

and a class Floor:

class Floor {

 public:
   Floor(Position p) : position(p) { }
 private:
   Position position;
    };

If I were to add a getter like getPosition(void), what should it return? Position? Position*? Position&? And should I have Position position or Position* position as instance variable? Or Position& position? Thanks.

Upvotes: 2

Views: 1573

Answers (1)

Joseph Thomson
Joseph Thomson

Reputation: 10413

By default, if you want a value of type T, use a data member of type T. It's simple and efficient.

Position position;

Usually, you will want to return this by value or by reference to const. I tend to return objects of fundamental type by value:

int getX() const
{
    return x;
}

and class objects by reference to const:

Position const& getPosition() const
{
    return position;
}

Objects that are expensive to copy will often benefit from being returned by reference to const. Some class objects may be quicker to return by value, but you'd have to benchmark to find out.


In the less common case where you want to allow the caller to modify your data member, you can return by reference:

Position& getPosition()
{
    return position;
}

However, it is usually better prevent direct access to class internals like this. It gives you more freedom to change implementation details of your class in the future.


If you need to dynamically allocate the value for some reason (e.g. the actual object may be one of a number of derived types at determined at runtime), you can use a data member of std::unique_ptr type:

std::unique_ptr<Position> position;

Create a new value using std::make_unique:

Floor() :
    position(std::make_unique<FunkyPosition>(4, 2))
{
}

Or move in an existing std::unique_ptr:

Floor(std::unique_ptr<Position> p) :
    position(std::move(p))
{
}

Note that you can still return by value or reference to const:

Position const& getPosition() const
{
    return *position;
}

That is, as long as position cannot contain nullptr. If it can, then you may want to return a pointer to const:

Position const* getPosition() const
{
    return position.get();
}

This is a genuine use of raw pointers in modern C++. Semantically, this communicates to the caller that the value returned is "optional" and may not exist. You should not return a reference to const std::unique_ptr<T>, because you can actually modify the value it points to:

std::unique_ptr<Position> const& getPosition() const
{
    return position;
}

*v.getPosition() = Position(4, 2); // oops

In addition, returning a std::unique_ptr would once again expose unnecessary implementation details, which you should prefer not to do.


It is also possible for multiple objects to own the same dynamically-allocated object. In this case, you can use std::shared_ptr:

std::shared_ptr<Position> position;

Create a new value using std::make_shared:

Floor() :
    position(std::make_shared<FunkyPosition>(4, 2))
{
}

Or copy/move in an existing std::shared_ptr:

Floor(std::shared_ptr<Position> p) :
    position(std::move(p))
{
}

Or move in an existing std::unique_ptr:

Floor(std::unique_ptr<Position> p) :
    position(std::move(p))
{
}

Only once all std::shared_ptrs pointing to an object have been destroyed is the object itself destroyed. This is a far heavier-weight wrapper than std::unique_ptr so use sparingly.


In the case where your object is referencing an object it doesn't own, you have several options.

If the referenced object is stored in a std::shared_ptr, you can use a data member of type std::weak_ptr:

std::weak_ptr<Position> position;

Construct it from an existing std::shared_ptr:

Floor(std::shared_ptr<Position> p) :
    position(std::move(p))
{
}

Or even another std::weak_ptr:

Floor(std::weak_ptr<Position> p) :
    position(std::move(p))
{
}

A std::weak_ptr does not own the object it references, so the object may or may not have been destroyed, depending on whether all its std::shared_ptr owners have been destroyed. You must lock the std::weak_ptr in order to access the object:

auto shared = weak.lock();
if (shared) // check the object hasn't been destroyed
{
    …
}

This is super safe, because you cannot accidentally dereference a pointer to a deleted object.


But what if the referenced object is not stored in a std::shared_ptr? What if it is stored in a std::unique_ptr, or isn't even stored in a smart pointer? In this case, you can store by reference or reference to const:

Position& position;

Construct it from another reference:

Floor(Position& p) :
    position(p)
{
}

And return by reference to const as usual:

Position const& getPosition() const
{
    return position;
}

Users of your class have to be careful though, because the lifetime of the referenced object is not managed by the class holding the reference; it's managed by them:

Position some_position;
Floor some_floor(some_position);

This means that if the object being referenced is destroyed before the object referencing it, then you have a dangling reference which must not be used:

auto some_position = std::make_unique<Position>(4, 2);
Floor some_floor(*some_position);
some_position = nullptr; // careful...
auto p = some_floor.getPosition(); // bad!

As long as this situation is carefully avoided, storing a reference as a data member is perfectly valid. In fact, it is an invaluable tool for efficient C++ software design.

The only problem with references is that you cannot change what they reference. This means that classes with reference data members cannot be copy assigned. This isn't a problem if your class doesn't need to be copyable, but if it does, you can use a pointer instead:

Position* position;

Construct it by taking the address of a reference:

Floor(Position& p) :
    position(&p)
{
}

And we return by reference to const as before:

Position const& getPosition() const
{
    return *position;
}

Note that the contructor takes a reference, not a pointer, because it prevents callers from passing a nullptr. We could of course take by pointer, but as mentioned earlier, this suggests to the caller that the value is optional:

Floor(Position* p) :
    position(p)
{
}

And once again we may wish to return by pointer to const:

Position const* getPosition() const
{
    return position;
}

And I think that's it. I spent far longer writing this (probably unnecessarily detailed) answer than I intended to. Hope it helps.

Upvotes: 4

Related Questions