2

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.

Garf365
  • 3,619
  • 5
  • 29
  • 41
David Frickert
  • 127
  • 2
  • 10
  • See this SO post: http://stackoverflow.com/questions/752658/is-the-practice-of-returning-a-c-reference-variable-evil Basically, my rule of thumb is that if the object is not significantly big, return by value (Assuming you don't need to change it externally). If it is, return by const reference (again, assuming you don't need to change it). Never use raw pointers - We have smart pointers for that (read about shared_ptr and unique_ptr) – Mr. Anderson Mar 14 '16 at 13:10
  • Do you want to allow the person whole calls `getPosition` to modify the position that `Floor` holds? – NathanOliver Mar 14 '16 at 13:11
  • @NathanOliver not really, i only want the person to be able to 'read' their 'x' and 'y' with the getters – David Frickert Mar 14 '16 at 13:12
  • The parameter list `(void)` is a C-ism. In C++ it is equivalent to `()`, which is the preferred form. – molbdnilo Mar 14 '16 at 13:22
  • @molbdnilo oh wow... didn't even know, i thought it was necessary, thanks – David Frickert Mar 14 '16 at 13:25

1 Answers1

4

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.

Joseph Thomson
  • 9,888
  • 1
  • 34
  • 38
  • The `const` before a reference or pointer, (`T const&` or `T const*`) means that the object being referenced cannot be modified via the reference/pointer. In our case, it stops the underlying `position` data member being modified by the caller. The `const` after the function parameter list means that the implicit `this` pointer becomes a pointer to const (e.g. `Floor const*`). This means that the function can be called invoked on a `const` object, and that the data members become `const` within the function. For example, you cannot have `position = Position(1, 0);` inside `getPosition`. – Joseph Thomson Mar 15 '16 at 18:00
  • @DavidFrickert I may have accidentally deleted your comment (unless you just deleted it). – Joseph Thomson Mar 15 '16 at 18:01
  • The correct use of `const` is known as _const correctness_, and is an important part of C++ programming. It increases the integrity of your code (knowing that some function won't modify an object you pass it is a big bonus). Wherever you do not need to modify an object, pass it by reference or pointer to `const`. Non-`const` member functions cannot be invoked on `const` objects, so make sure you also make them `const` wherever possible, to maximize their utility. – Joseph Thomson Mar 15 '16 at 18:10
  • One warning about pointers/references: they don't propagate `const`ness. E.g. if `position` is of type `Position*`, inside `getPosition` it will only be accessible as a `Pointer* const`. Note the `const` is to the right of the `*`. A const pointer is not the same as a pointer to const. The former cannot itself be modified to point to another object, but the underlying object is still accessible as a non-`const`. This is why returning `std::unique_ptr const&` (which is like returning `Position* const&`) is bad: dereferencing it gives a non-`const` reference, `Position&`. – Joseph Thomson Mar 15 '16 at 18:20
  • It was me that deleted it, because I searched some other SO questions and experimented a bit, and understood how it works, more or less. Thanks for everything, that helped a lot! – David Frickert Mar 15 '16 at 20:31