3

I'm new to C++, coming from Java.

In Java, all variables (except for primitives) are essentially pointers. They hold the address of whatever they're 'holding'.

So any Java data structure stores it's data by reference. You can also store by value, i.e. save and return a copy of any item you store, but that would take extra work and isn't native to the language.

For example, the collections ArrayList, HashSet, and a simple array all store the addresses of the items they 'store', and not the actual items.

However in C++, you have a choice: when implementing a container class, you can either store and return to the user items by value or by reference.

For example, here's a simple Stack class I wrote (omitted irrelevant stuff):

template <typename T> class Stack {
public:
    Stack(...) : ... { }

    void push(const T& item) {
        if(size == capacity - 1)
            enlargeArray();

        data[indexToInsert++] = &item;
        size++;
    }

    const T& pop() {
        const T& item = *data[indexToInsert - 1];
        data[indexToInsert - 1] = 0;
        indexToInsert--;
        size--;
        return item;
    }

    int getSize() const {
        return size;
    }
private:
    const T** data;
    int indexToInsert;
    int size;
    int capacity;

    void enlargeArray() {
        // omitted
    }
};

This data structure takes and returns data by reference. push takes a const reference, and pop returns a const reference. The backing array is an array of pointers, not objects.

However push could also look like so:

    void push(T item) {
        if(size == capacity - 1)
            enlargeArray();

        data[indexToInsert++] = item;
        size++;
    }

And pop could return a T, not a const T&, etc.

My question is: what is the preferred approach in C++? Is there a preferred approach? Which approach should I normally take when implementing 'container' classes?

  • 1
    C++ does have a stack in standard library. It's creatively named std::stack so it shouldn't be hard to find. And this one is there since the initial SGI STL. You don't need to implement it. – Jan Hudec Oct 6 '14 at 8:46
  • @JanHudec Of course, it's just for learning :) – Aviv Cohn Oct 6 '14 at 9:48
  • 2
    One of the general themes in the answers below and in the c++ vs java worlds, is that c++ templates are extremely powerful, and have less of the limitations of Java generics. As people have answered, you can write the code once and support both value and pointer semantics for your clients. Also note that your templated code supports all c++ types, including primitives (unlike Java generics). So... enjoy templates :-) – Nir Friedman Oct 6 '14 at 16:01
11

Firstly, you probably shouldn't implement a container class. 95% of the time you should one included in the standard library. If you just want to learn, or are in the 5%, carry on.

If you are defining a template, leave the decision up to your users. You users can use:

Stack<Foo> if they want by value. Stack<Foo*> if they want by pointer. Stack<std::unique_ptr<Foo>> if they want pointers that clean up after themselves.

When choosing which to use, you should default to by value, unless you've got a good reason to do something different. Inside your stack class, just store everything by value. If the use of the template needs indirection, they can use T=pointer type.

Looking at your code:

void push(const T& item) {
    if(size == capacity - 1)
        enlargeArray();

    data[indexToInsert++] = &item;
    size++;
}

You can't do that. &item records the pointer to whatever was passed in. But you have no idea how long the pointer will be valid for. It could become invalid right after push finished. In that case, you've stored a pointer to an invalid place. In general, you can't assume that a pointer remains valid. You should instead be copying the item.

  • 1
    don't put raw pointers in containers - especially for a newbie. Stick to shared_ptr or unique_ptr for safety, then the issue becomes one of stack v heap allocation of objects (and yes, prefer stack) – gbjbaanb Oct 6 '14 at 7:46
  • What do you mean by "how long the pointer will be valid for"? By "valid" do you mean a pointer that points to an address that holds what you expect, and invalid is a pointer that points to an address that doesnt contain what you expect? – Aviv Cohn Oct 6 '14 at 9:54
  • @AvivCohn: It means that the object that the pointer points at may have been destroyed. Note that C++ doesn't have garbage collection like Java and can destroy objects while you still have pointers to them. – Bart van Ingen Schenau Oct 6 '14 at 11:40
  • Winston: I understand I should get and store values by value. But what about return them? Should I return them by value (i.e. make a copy of the stored object and return that), but reference (i.e. the client can affect the object inside the container), or by const reference? – Aviv Cohn Oct 6 '14 at 17:16
  • @AvivCohn, it depends. If the user can't break your container by modifying the object return a reference. If the user can break your container by modifying the object return a const reference. – Winston Ewert Oct 6 '14 at 19:44
4

My question is: what is the preferred approach in C++? Is there a preferred approach? Which approach should I normally take when implementing 'container' classes?

In C++ you can keep objects by:

  • value
  • reference
  • pointer
  • smart pointer (std::unique_ptr, std::shared_ptr, YourPointerClass). (you didn't mention the last two).

Each of these is valid for different situations and imposes different constraints, concerning objects ownership, lifetime management and polymorphic behavior ( that is, there isn't a prefered approach - there are many of them :) ):

  • use storing by value when:

    • you are not storing objects with polymorphic behavior (i.e. objects are concrete classes with no base class and no virtual functions, or all objects are of the same runtime type - you are not storing specializations of a base class)
    • your container owns the objects (i.e. the contained objects should have a lifetime equal to that of the container, and will be destroyed when the container is destroyed)
  • use storing by reference when:

    • the stored objects are not owned by the container
    • your contained objects have a larger scope and lifetime than the container.
    • you are interested in polymorphic behavior (in this case you should store references to a base class) Normally you should not do this, as there is a chance of slicing on assignment (unless your class hierarchy supports polymorphic assignment - which is another discussion altogether)
  • use storing by raw pointer when:

    • your stored objects expose polymorphic behavior (base class and/or virtual functions)
    • the container doesn't own the stored objects
  • use storing by smart pointer when:

    • your stored objects expose polymorphic behavior
    • your objects are owned by the container (std::unique_ptr), ownership is shared (std::shared_ptr) or osage of smart pointer is imposed by client code constraints.
    • the stored objects are expensive to copy/instantiate (edit cf. @NirFriedman)

To support all this, you will probably want to template your class by the stored type and fill in as needed, in client code.

  • I suggest to the author: amend to mention using smart pointers over value when the objects are large and you could be moving them a lot. – Nir Friedman Oct 6 '14 at 15:51
  • Why to use raw pointers even if container doesn't own them? If those objects get deleted then pointers become invalid. In complex software its better to use smart pointers unless in limited situations. – Shital Shah Dec 15 '16 at 22:50
0

Depends on if the container "owns" the object and they are a base class.

  1. If the container doesn't own the object then you should use pointers and be careful of the danglers (make sure no container holds a pointer to the object when it is destroyed).

  2. If the container owns the object and is not a base class then you should store by value.

  3. Otherwise (container owns and there are subclasses) you should store using a std::unique_ptr to ensure no slicing happens on store and proper destruction.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.