1

I've trying to find some information towards casting class values within their hierarchy, but I have only been able to find useful information about casting pointers to classes.

So here we go:

#include <map>
#include <string>
#include <iostream>

class Base {
protected:
  std::map<std::string, std::string> properties;
};

class Sub: public Base {
public:
  std::string &first_name() {
    return properties["first_name"];
  }
  std::string &last_name() {
    return properties["last_name"];
  }
};

Base factory() {
  Sub sub;
  sub.first_name() = "John";
  sub.last_name() = "Doe";
  return sub;
}

int main() {
  Base base(factory());
  Sub sub(static_cast<const Sub &>(base));
  std::cout << "First name: " << sub.first_name() << std::endl;
  std::cout << "Last name: " << sub.last_name() << std::endl;
  return 0;
}

Is the behaviour of the program above problematic or is it well-defined? I am basically dealing with subclasses of a base class, where only the base class has attributes. All subclasses only have functions. Is it an issue if the objects are freely converted from their base to the sub and back?

Pascal Kesseli
  • 1,620
  • 1
  • 21
  • 37

3 Answers3

2

Your factory function returns by value so the Sub is sliced in the return, changing the returned type to Base. Then when you downcast to Sub you have undefined behavior (because you can't cast an object to a type it is not under any circumstances) and all bets are off.

If you actually had a Sub object that was temporarily treated as a Base pointer or reference it would be perfectly legal to cast it back to a Sub. In this case as soon as the factory function returns you no longer have a subclass object because the derived parts have been sliced away.

Mark B
  • 95,107
  • 10
  • 109
  • 188
  • Value-based downcast is undefined behaviour? You wouldn't happen to have a source for that? – Pascal Kesseli Aug 15 '14 at 19:48
  • @PascalKesseli There is no "value-based downcast" like you describe in C++. `base` is not an instance of `Sub`, so you can't treat it like one. – Oktalist Aug 15 '14 at 21:04
  • But I guess that casting to and from subclasses which have no additional member variables is perfectly viable, though not really recommended... Slicing has no effect in this case – Savail Aug 16 '14 at 08:17
  • @Savail Even in that case it's still totally illegal as far as the language is concerned and the compiler is under no obligation to make your code work the way you expect. – Mark B Aug 16 '14 at 21:22
1
Base factory() {
  Sub sub;
  return sub;
}

The first line of this function constructs an instance of Sub. The second line creates a completely separate instance of Base which is a copy of the Base part of the instance from the first line. The original Sub instance is destroyed before the function returns. The fact that the returned object was constructed by copying from a Sub is not retained, nor would it be useful if it were. The returned object is just a Base, not a Sub. It's undefined to cast it to Sub& for the same reason that it's undefined to cast a float to double& (although static_cast will not allow you to do the latter).

This is value semantics, a frequent cause of confusion to programmers coming from languages like Java and C#. If you want reference semantics like you are used to in those languages, you have to ask for it by using a reference or a smart pointer.

If you actually want to construct a local instance of Sub from the Base instance returned by factory() (the only sensible meaning of "value-based downcast"), that's easy if you add an extra constructor in Sub:

class Sub : public Base {
public:
  Sub(const Base &base) : Base(base) {}
  //...
};

Base factory()
{
  Base base;
  return base;
}

int main() {
  Sub sub(factory());
  std::cout << sub.first_name() << " " << sub.last_name() << "\n";
}

You might find it a better design to use composition instead of inheritance here.

Oktalist
  • 14,336
  • 3
  • 43
  • 63
0

With the restrictions mentioned in the question, this is actually defined behaviour. This is a pattern used in the C++ code of the static analyser CBMC, for example: https://github.com/diffblue/cbmc/blob/develop/src/util/irep.h

Most entity classes in the code base inherit from irept. They are not allowed to introduce instance member variables themselves though, only member functions. All data should be stored in irept::sub and irept::named_sub. So long as no instance member variables are introduced by subclasses, no actual slicing occurs in the provided example code on return.

/// To simplify this process, there are a variety of classes that inherit
/// from \ref irept, roughly corresponding to the ids listed (i.e. `ID_or`
/// (the string "or”) corresponds to the class \ref or_exprt). These give
/// semantically relevant accessor functions for the data; effectively
/// different APIs for the same underlying data structure. None of these
/// classes add fields (only methods) and so static casting can be used. The
/// inheritance graph of the subclasses of \ref irept is a useful starting
/// point for working out how to manipulate data.
Pascal Kesseli
  • 1,620
  • 1
  • 21
  • 37