10

I'm fairly new to C++ and this is the problem I have: I have two classes, Client and Host. And when everything is loaded you have the option to press two buttons, if you press button 1 Client is loaded and if you press button 2 Host is loaded.

Now both Client and Host are fairly big classes, and I don't want to put them both into the memory. So my idea was creating a Base class, and then both Client and Host should extend the base class, and then the only thing I had to do was this:

Base connection;

//If button 1 is pressed:
connection = Client();

//If button 2 is pressed:
connection = Host();

Well this sounded almost too good to be true, and when I tried it I got no errors. Now comes the problem, Base has a function called A, and Client has a function called B. So the function B is unique to the class Client.

When I try to call function B I get this error: 'class Base' has no member named 'B'. How can I let C++ know that I am talking to class Client or Host instead of Base? I am also open for a whole new approach to this problem. Maybe it's just an error in my thinking process.

Thanks in advance!

tversteeg
  • 4,717
  • 10
  • 42
  • 77
  • 4
    Questions related to object slicing seem popular recently. – Jesse Good Jan 23 '13 at 23:41
  • Your code probably doesn't do what you thing it is doing. Look up *object slicing*. – juanchopanza Jan 23 '13 at 23:42
  • 4
    You are *instantiating* a Client or Host respectively. However, you assign them to a Base variable, which *converts* them (well, in fact it *copies*) to a new Base instance, so the original identity gets lost, also known as object slicing. – leemes Jan 23 '13 at 23:42
  • I've read a quick bit on Object Slicing but is there a way I can choose runtime between the two classes without having them both in the memory? – tversteeg Jan 23 '13 at 23:46
  • What exactly are those different functions, `A` and `B`? There may be a more fundamental behaviour which needs abstracting. – Peter Wood Jan 23 '13 at 23:51
  • Well it's just an example in this case, but in my app `B` loads just the socket, and `A` uses that socket to send data to the host. Which could also be selected and then it would have a function called `C` – tversteeg Jan 23 '13 at 23:53
  • You mean `C` calls either `A` or `B`, depending on the type? Then just call it `C` and make it virtual in `Base`, and overwrite it in the two subclasses. – leemes Jan 23 '13 at 23:54
  • No I explained it wrong, sorry; `A` `B` and `C` never call each other. They are just different classes called in a different situation, `A` is always called; `B` is called when `Client` is loaded and `C` is called when `Host` is loaded. – tversteeg Jan 23 '13 at 23:58

4 Answers4

6

You ran into a situation which we call object slicing, which is a common problem in languages with value semantics such as C++.

Object slicing happens when you assign a value of a sub-type to a new location (your variable connection) of a super type. This introduces a new copy of the instance, but of the super type, not the sub-type, so you lose the information about the concrete class you wanted to instantiate.

To avoid this, you have multiple options.

The classical approach uses pointers:

Base * connection;
connection = new YourConcreteType();

Then, to use this object, you have to derefrerence it using the asterisk operator (*):

(*connection).function();
connection->function();    // syntactic sugar, semantically equivalent

Not to forget: You have to delete the object after usage:

delete connection;

To simplify this, C++ introduces two concepts: references and smart pointers. While the former has a restriction to be only assigned once, it is the syntactically simplest one. The latter is similar to the pointer approach, but you don't have to care about deletion, so you less likely run into a memory leak situation:

std::shared_ptr<Base> connection;

connection = make_shared<YourConcreteType>(); // construction via 'make_shared'

// ... use as if it were just a pointer ...

connection->function();

// no delete!

There are also other "smart pointer" types, like unique_ptr, which can be used if you do not intend to pass the pointer around (if it stays in scope).

Now, you can implement the functions in both classes separately. To make use of polymorphism, this means, during runtime, either the function of the one subclass or of the other subclass is called, depending on which one was constructed, you should declare the functions in the base class as being virtual, otherwise, the function definition in Base will be called, regardless of the concrete type you have constructed.

In your case, you want to call a function which should do something different, depending on the type. While your approach was to introduce two different functions, namely A and B, you can just declare a single function, let's call it handleEvent, as a pure virtual (= abstract) function in the base class, which means "this function is to be implemented in sub classes", and define it in the two subclasses independently:

Base {
    ....
    virtual void handleEvent(...) = 0; // "= 0" means "pure"
};

// Don't provide an implementation

Client {
    void handleEvent(...); // override it
};

// define it for Client:
void Client::handleEvent(...) {
    ...
}

Host {
    void handleEvent(...); // override it
};

// define it for Host:
void Host::handleEvent(...) {
    ...
}
leemes
  • 44,967
  • 21
  • 135
  • 183
  • ``unique_ptr`` should be the default smart pointer. It isn't clear at all that shared ownership is required. – juanchopanza Jan 24 '13 at 00:03
  • Thank you very much for the great explanation, I think I understand what you mean. So if I get it correct I just have to create a sort of "prototype" for `B` and `C` in the `Base` class, which is then edited in `Client` and `Host`. After that I call the `Base` class as a pointer so it can be changed but since the functions have already been initialized in the `Base` class with the "prototype" function I only have to reference the memory. Am I correct? – tversteeg Jan 24 '13 at 00:05
  • @juanchopanza Well, there are some catches and I think a shared pointer is easier to use in general, especially to newbies. Of course, it is a good thing to mention that unique pointers can be used in a lot of cases (but not all, for example in a container) and that they are more efficient. – leemes Jan 24 '13 at 00:07
  • @ThomasVersteeg You got it 100% correct. :) Glad I could help. Remember to put this `virtual`. If you don't, it's still *valid code*. Without this, you can still "override" = "change the behavior of" a function in a subclass, that's not the problem. But if you call the function via a *Base pointer*, the Base function will be called, seriously not what you want! :) – leemes Jan 24 '13 at 00:08
  • I think that is the wrong approach. Teach people to do the wrong thing, and maybe they will carry on doing it forever, oblivious to the fact that there are better solutions. Plus, ``unique_ptrs`` *can* be used in containers, and *should* be, as a replacement for raw pointers to dynamically allocated objects. – juanchopanza Jan 24 '13 at 00:09
  • @leemes Thanks again, you really helped me ahead :), and also nice to know about the smart pointers, now I have some researching to do! But juanchopanze, isn't the way leemes explained just another way of fixing the problem? Or is it really a bad practice? – tversteeg Jan 24 '13 at 00:12
  • @juanchopanza Since C++11, yes. Ok I mostly agree with your argument about that "teaching the wrong way". But if you strength your argument with the fact that "there is a better solution": Smart pointers just use reference counting. This is a smart way to handle references in a program. Making them unique is an optimization. You can now turn your argument into: Don't use Java. They don't optimize pointer handling, they use only "shared pointers" internally, so don't use Java, because there is something better. ;) – leemes Jan 24 '13 at 00:13
  • @ThomasVersteeg the solution seems perfectly suitable to me. I just have an issue with the type of smart pointer, which will make sense if you manage to read up on C++11 smart pointers. – juanchopanza Jan 24 '13 at 00:15
  • @ThomasVersteeg The thing we are discussing about is: There are different types of pointers for different situations. Basically shared pointers (used to be passed around; shared ownership) and unique pointers (one dedicated owner). You can learn about them in the internet and a lot of literature. If you are willing to learn, it's a very good idea to know them with their strengths and catches. – leemes Jan 24 '13 at 00:15
  • @juanchopanza The thing is, I'm still programming some of my applications with C++03 compatibility, so I'm new to move semantics. Thanks for the hint anyway. Not that I want to say that shared pointers are the better solution in this case. ;) – leemes Jan 24 '13 at 00:17
  • 1
    @leemes Oh yes I am definitely interested in learning about the ins and outs of C++ programming, that's why I am so happy about this discussion :) – tversteeg Jan 24 '13 at 00:18
  • 1
    @ThomasVersteeg Rarity on Stack Overflow, sadly. Well, then [buy a book](http://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list) and have a lot of fun learning C++! Good luck ;) – leemes Jan 24 '13 at 00:20
  • @leemes Oh wow thanks for the links, I am definitely buying some of that books! – tversteeg Jan 24 '13 at 00:22
3

When you do this:

Base connection;

You create an object connection that has exactly the memory needed for type Base.

When you do this:

connection = Client();

you are not expanding the memory allocated or converting connection into a Client instance. Instead, you are creating a bigger object that gets "sliced" into a smaller one.

What you want to do, is use pointers (or references or smart pointers) so that what you keep is just an address of an object that might be one type or the other type. Like this:

Base *connection;
...
connection = new Client();

The first statement creates a pointer to a Base typed object, and the second one allocates the memory for a Client typed one, initializes it and assigns its address to connection.

imreal
  • 10,178
  • 2
  • 32
  • 48
  • When I do what you do, and call it like this `connection->A()` I still get the `'class Base' has no member named 'sendInput'` error! – tversteeg Jan 23 '13 at 23:48
  • Typically when you have derived classes from a base class, is because they share a common interface. So you might want to declare `A()` as a virtual function on `Base`, or use a dynamic cast (not advisable). `dynamic_cast(connection)->A()`. – imreal Jan 23 '13 at 23:52
2

If you want to use Base still then you are going to have to do this to use those functions:

if (Button1) {
    dynamic_cast<Client*>(connection)->A();
} else {
    dynamic_cast<Host*>(connection)->B();   
}  

And you will need to make connection a pointer. Base * connection.

This isn't really ideal though. You should investigate a different way to do it like in the other answers.

Fantastic Mr Fox
  • 32,495
  • 27
  • 95
  • 175
  • 2
    The dynamic_casts would need to have to use -> syntax (they resolve to be pointers), and you would need to deal with the case where the connection wasn't of the appropriate type (wherein dynamic_cast returns nullptr). If you're absolutely sure your types are 'Client' (when Button1 is trie) and 'Host' otherwise, you can use a static_cast<> to downcast to the appropriate type. However; this approach is generally to be avoided and usually indicates a design issue that can be fixed using the approaches mentioned above. – Matt Godbolt Jan 24 '13 at 00:20
2

First, this line

connection = Client();

is using the assignment operator to set the state of connection, a Base, from a temporary Client object. connection is still a Base object. What you can do is the following:

std::unique_ptr<Base> makeBase(someFlagType flag)
{
  if (flag) {
    return std::unique_ptr<Base>(new Cient);
  } else {
    return std::unique_ptr<Base>(new Host);
  }
}

Then

std::unique_ptr<Base> b = makeBase(myFlag);
b->someBaseMethod();

Concerning the casting part, I would say that if you find yourself having to cast to a child type, you should re-think the design of the classes.

juanchopanza
  • 223,364
  • 34
  • 402
  • 480