3

I'm fairly new to C++, and I'm trying to understand the good practices for building classes.

Let's say I have a class Foo:

class Foo {
  public:
    double foo;
    Foo(double foo);
    Foo add(Foo f);
}

I want to make a class Bar, that is made of two Foo objects, and that creates a third one at construction.

1st option: Objects as class members

class Bar {
  public:
    Foo foo1;
    Foo foo2;
    Foo foo3;
    Bar(const Foo &f1, const Foo &f2);  
}

Bar::Bar(const Foo &f1, const Foo &f2):
{
  foo1 = f1;
  foo2 = f2;
  foo3 = f1.add(f2);
}

As is, it does not work as I have not defined a default constructor for Foo.

2nd option: Pointers as class members

class Bar {
  public:
    const Foo* foo1;
    const Foo* foo2;
    const Foo* foo3;
    Bar(const Foo &f1, const Foo &f2);  
}

Bar::Bar(const Foo &f1, const Foo &f2):
{
  foo1 = &f1;
  foo2 = &f2;
  foo3 = &(f1.add(f2));
}

Note: I have to declare foo1 and foo2 as const for the constructor to work. It still fails though because for foo3 I am taking the address of a temporary result, which is illegal.


Which option is more natural (and how can I fix the errors)? I feel the first option is probably better, but then my Foo objects have to be created twice in memory, don't they? (once to call the constructor, and a second time by the constructor itself)

Any help appreciated.

Eric Leibenguth
  • 4,167
  • 3
  • 24
  • 51
  • 3
    First off *I have not defined a default constructor*: use the [member initializer list](http://en.cppreference.com/w/cpp/language/initializer_list) in such cases. Second: use pointers do reference things, and `std::unique_ptr` for ownership. Third: avoid unnecessary dynamic allocations, try coding with value types and allocate only when really necessary (i.e. for containers). – BeyelerStudios Feb 19 '16 at 17:00
  • 1
    Ask yourself, if the objects pointed by the pointers are ever distructed, and when, by whom. If the answers are clear then the lifetime of the objects are under control then everything works. – user3528438 Feb 19 '16 at 17:00
  • If you don't have a default constructor for `Foo` in the 1st sample, you can still initialize it properly using the member initializer list. – πάντα ῥεῖ Feb 19 '16 at 17:01
  • And whenever you save a pointer or a reference you need to understand you are sharing an object that is owned by others, so either pass the ownership together with the pointer/reference, or make sure when you use the pointer/reference, the target still exists and is not moved. – user3528438 Feb 19 '16 at 17:03
  • Possible duplicate of [C++ - when should I use a pointer member in a class](http://stackoverflow.com/questions/1175646/c-when-should-i-use-a-pointer-member-in-a-class) – Francesca Nannizzi Feb 19 '16 at 17:04
  • 1
    You second option is just plain terrible. You are taking an address of something which was passed to you as const reference. It could be a temporary! – SergeyA Feb 19 '16 at 17:04
  • 1
    @bullsy: That question is clearly a duplicate, but the answers there predate C++11... And any answer to this question that fails at least to mention `std::unique_ptr` is wrong. Not sure what the right SO protocol is in this sort of case. – Nemo Feb 19 '16 at 17:15
  • @BeyelerStudios: Your comment is better than all of these answers combined. Care to elaborate it into an actual answer? – Nemo Feb 19 '16 at 17:15
  • When using pointers decide on whether the target object is copied during the containing class' assignment or copy operations (shallow vs. deep copy). For shallow copy, use a pointer that has reference counting capability. – Thomas Matthews Feb 19 '16 at 17:20
  • As you can see by comments and answers, your question is way too broad. You are asking us something which is normally covered in entire book chapters. Here are some ultra-brief guidelines: (1) avoid dynamic allocation if you can, (2) prefer references to pointers, (3) understand the difference between owning and non-owning pointers, (4) only use smart pointers for owning pointers, (5) break rule 4 if you are implementing your own really low-level data structure. Read http://herbsutter.com/elements-of-modern-c-style/ – Christian Hackl Feb 19 '16 at 18:52
  • @Nemo If that other question is frequently used as a duplicate target, consider adding a new, up-to-date answer. It's better to keep information in one question, rather than having separate old and new questions, because a user who finds the old question might walk away without ever seeing the new one. – Jeffrey Bosboom Feb 20 '16 at 00:03

3 Answers3

6

It's fine to use pointers as members, but in your case you are simply working around a minor hiccup that really doesn't warrant the use of pointers, and using pointers can be dangerous as evidenced by an issue I'll point out shortly.

As is, it does not work as I have not defined a default constructor for Foo.

This is easily resolved by using initializers for Bar:

Bar(const Foo &f1, const Foo &f2) : foo1(f1), foo2(f2), foo3(f1.add(f2)) {}

as demonstrated here:

#include <iostream>

class Foo {
  public:
    double m_foo;
    Foo(double foo) : m_foo(foo) {}
    Foo add(Foo f) { f.m_foo += m_foo; return f; } // returns temporary!
};

class Bar {
  public:
    Foo m_foo1;
    Foo m_foo2;
    Foo m_foo3;
    Bar(const Foo &foo1, const Foo &foo2);  
};

Bar::Bar(const Foo &foo1, const Foo &foo2)
    : m_foo1(foo1)
    , m_foo2(foo2)
    , m_foo3(m_foo1.add(m_foo2))
{
}

int main() {
    Foo foo1(20.0);
    Foo foo2(22.0);
    Bar bar(foo1, foo2);

    std::cout << bar.m_foo3.m_foo << "\n";

    return 0;
}

Live demo: http://ideone.com/iaNzJv

In your pointer solution you introduce a glaring pointer problem: a pointer to a temporary.

foo3 = &(f1.add(f2));

f1.add returns a temporary Foo, which you take the address of, and then it goes away. This is a dangling pointer.

Your pointer implementation also doesn't explicitly take pointers as its inputs so f1 and f2 could run into the same problem:

Bar(Foo(20), Foo(22));  // two temporary Foos passed by reference
                        // but having their addresses taken. ouch.

If you're taking pointers, it's best to do that at the api to your class; you're going to have to care about the lifetime of the things pointed to, and try to make it easier for a caller to tell that you are doing so.

Bar(Foo* f1, Foo* f2);

But now if you're going to have F3 you're going to be responsible for managing it's memory:

Bar(Foo* f1, Foo* f2)
    : foo1(f1), foo2(f3), foo3(new Foo(*f1.add(*f2)))
{}

~Bar()
{
    delete f3;
}

So in your example case, using members is probably drastically better.

Save the use of pointers for large objects that you definitely don't want to copy, and where you can't use a move operation.

--- EDIT ---

The problem of conveying ownership of pointers has been largely solved in modern C++ (C++11 and higher), through "smart pointers", in particular std::unique_ptr and std::shared_ptr.

It is generally considered Best Practice to use these instead of raw pointers, although it requires learning some newer C++ concepts.

#include <memory>

struct Foo {};
class Bar {
public:
    std::unique_ptr<Foo> m_f1; // we will own this
    std::unique_ptr<Foo> m_f2; // and this

    Bar(std::unique_ptr<Foo> f1) // caller must pass ownership
        : m_f1(std::move(f1))    // assume ownership
        , m_f2(std::make_unique<Foo>()) // create a new object
    {}

    ~Bar()
    {
        // nothing to do here
    }
};

int main() {
    auto f = std::make_unique<Foo>();
    Bar(std::move(f)); // the 'move' emphasizes that
                       // we're giving you ownership
    // 'f' is now invalid.

    return 0;
}

Live demo: http://ideone.com/9BtGkn

The elegance of this is that when Bar goes out of scope, the unique_ptrs will ensure that the objects they own are destroyed for us -- we don't have to remember to delete them.

In the above example, it would probably have been much better to make m_f2 a member rather than a pointer.

kfsone
  • 23,617
  • 2
  • 42
  • 74
  • Using raw pointers for ownership is extremely poor practice for multiple reasons, as any experienced C++ programmer knows. -1. – Nemo Feb 19 '16 at 17:40
  • @Nemo, same mistake again. There is no sign of **owning** the pointer. Your downvote is unwarranted. – SergeyA Feb 19 '16 at 17:48
  • @SergeyA: Read the last few paragraphs, starting from "...you're going to be responsible for managing...". Uses raw pointer for ownership, which merits my downvote. (Actually, I would claim any answer here that does not carefully distinguish owning from non-owning pointers is bad. As our own argument shows.) – Nemo Feb 19 '16 at 18:19
4

If the objects are not too expensive to pass around, I suggest using objects as members.

If you need to use pointers for some reason, you need to have ownership policies in place. Does Bar object own the objects? Does Bar just holds the pointers to the objects but is not responsible for releasing resources used by them?

If Bar owns the Foo objects, prefer to use one of the smart pointers. You'll need to make copies of those objects by using new and hold on to those pointers.

Here's how I see it:

class Bar {
  public:
    std::unique_ptr<Foo> foo1;
    std::unique_ptr<Foo> foo2;
    std::unique_ptr<Foo> foo3;
    Bar(const Foo &f1, const Foo &f2) : foo1(new Foo(f1)), ... {}
};

std::unique_ptr does not have a copy constructor. Hence, you must provide a copy constructor for Bar and initialize its members from the copy appropriately.

If Bar does not own the Foo objects, you might be able to get by using references as member data.

class Bar {
  public:
    Foo const& foo1;
    Foo const& foo2;
    Foo const& foo3;
    Bar(const Foo &f1, const Foo &f2) : foo1(f1), ... {}
};
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • Another cargo-cult. 'prefer to use one of the smart pointers' is wrong, and any using an object as a member or non-member generally does not depend on expense of 'passing it around'. 'initialize its members from the copy appropriately' is simply not enough as an answer. – SergeyA Feb 19 '16 at 17:18
  • 1
    Even if objects are expensive to pass around they are need not necessarily be shared, you can still `move` them. – user3528438 Feb 19 '16 at 17:19
  • @SergeyA, I would love to see an answer from you for this question. – R Sahu Feb 19 '16 at 17:22
  • RSahu, the best answer was already given by @kfsone, I have nothing to add. – SergeyA Feb 19 '16 at 17:27
  • @SergeyA: It's not "cargo cult"; it's solid C++ design. Using raw pointers for ownership makes exception-safe code essentially impossible to write. `std::unique_ptr` imposes zero overhead, both in space and in time, and results in simpler and safer code. No decent C++ programmer uses raw pointers for ownership, ever. – Nemo Feb 19 '16 at 17:42
  • @Nemo, original answer says nothing about ownership. It makes a generic, wrong statement (*'If you need to use pointers for some reason, prefer to use one of the smart pointers.'*). Owning pointers should be a smart pointer, and no one argues about that. However, there is a multitude of cases when non-owning pointer must be used. – SergeyA Feb 19 '16 at 17:47
  • @SergeyA, I see where you are coming from. – R Sahu Feb 19 '16 at 17:53
  • Storing const& references is not a great idea. You are inhibiting assignments, for no reason whatsoever. – SergeyA Feb 19 '16 at 18:00
  • @SergeyA, using references does not inhibit assignments. I don't know why you think that. – R Sahu Feb 19 '16 at 18:04
  • @RSahu: Well, a `const` reference member certainly inhibits assignment of that member... – Christian Hackl Feb 19 '16 at 18:39
  • @ChristianHackl, yes, it makes sense now. – R Sahu Feb 19 '16 at 18:41
0

I think it is nonsense that the object is the same as a primitive variable.

class Foo {
  public:
    double _stocks;
    Business* _business;
    Foo(double stocks, Business* business):_stocks(stocks), _business(business){}
    Foo* add(const Foo& f) {
        _stocks += f._stocks;
        _busines->merge(f._business);
        return this;
    }
    virtual ~Foo() {  delete _business;  }
}
class Bar {
  public:
    Foo* _foo1;
    Foo* _foosub;
//    Foo* _foo3;
    Bar(Foo* f1, Foo* f2); // unable const for f1 at least 
}
Bar::Bar(Foo* f1, Foo* f2):
{
    _foo1 = f1;
    _foosub = f2;
    _foo1.add(*f2);
    // _foo3 is the same as _foo1
}
void main() {
    Foo company1(100.00, BusinessFactory.create("car"));
    Foo company2(2000.00, BusinessFactory.create("food"));
    Bar conglomerate(&company1, &company2);
    // to be continued
}
nariuji
  • 288
  • 1
  • 6