0

I try to have an abstract base class act like an interface and instantiate a derived class based on user input. I tried to implement it like this

class A 
{
    public:
        virtual void print() = 0;   
};

class B : A
{
    public:
        void print() override { cout << "foo"; }
};

class C : A
{
    public:
        void print() override { cout << "bar"; }
};

int main()
{   
    bool q = getUserInput();
    A a = q ? B() : C();
    a.print();  
}

But that does not work. I’m coming from c# and that would be valid c# so I’m looking for an equivalent way of implement it in c++. Could someone please give me a hint? Thanks!

AILogic
  • 25
  • 3
  • 5
    You're encountering [object slicing](https://stackoverflow.com/questions/274626/what-is-object-slicing). If you want to use polymorphism in this fashion you'll want to have something like a smart pointer to an `A` rather than an automatic storage duration variable. You'll also want to give `A` a [virtual destructor](https://stackoverflow.com/questions/461203/when-to-use-virtual-destructors) in this case. – Nathan Pierson Apr 20 '21 at 19:12
  • You're used to `class` indicating a reference type in C#, when in C++ it's more like `struct`; we manage our references (pointers) more manually. You can cast a *pointer* to a base class safely, but when you cast a value type to a base class value type, you slice off the derived class' stuff. – AndyG Apr 20 '21 at 19:13
  • Thanks! I tried to implement the smart pointer version, but could not get it to work. I tried something like std::unique_ptr a = q ? std::unique_ptr(new B()) : std::unique_ptr(new C()); but I guess covariance does not work here. – AILogic Apr 20 '21 at 19:49
  • In addition to answer given: you probably have to start from basics and learn concept of C++ storage classes, memory model and of class-types memory layout and understand how it is different from C#. The "what" and "why" is going on in your code is clearly explained in first chapters of standard and you are apparently confused by assumption that C++ and C# can be similar. C# distanced from C++ further away than Java and Java got similar issues of clouding judgement. – Swift - Friday Pie Apr 21 '21 at 06:02

2 Answers2

3

There are two problems with the code in its current state.

  1. By default in C++, a class that inherits from a class or struct will be private inheritance. E.g. when you say class B : A, it's the same as writing class B : private A -- which in C++ restricts the visibility of this relationship only to B and A.

    This is important because it means that you simply cannot upcast to an A from outside the context of these classes.

  2. You are trying to upcast an object rather than a pointer or reference to an object. This fundamentally cannot work with abstract classes and will yield a compile-error even if the code was well formed.

    If the base class weren't abstract, then this would succeed -- but would perform object slicing which prevents the virtual dispatch that you would expect (e.g. it won't behave polymorphically, and any data from the derived class is not present in the base class).

To fix this, you need to change the inheritance to explicitly be public, and you should be using either pointers or references for the dynamic dispatch. For example:

class A 
{
public:
    virtual void print() = 0;   
};

class B : public A
//        ^~~~~~
{
public:
    void print() override { cout << "foo"; }
};

class C : public A
//        ^~~~~~
{
public:
    void print() override { cout << "bar"; }
};

To model something closer to the likes of C#, you will want to construct a new object. With the change above to public, it should be possible to use std::unique_ptr (for unique ownership) or std::shared_ptr (for shared ownership).

After this, you can simply do:

int main() {
    auto a = std::unique_ptr<A>{nullptr};
    
    auto q = getUserInput();
    
    if (q) { // Note: ternary doesn't work here
        a = std::make_unique<B>();
    } else {
        a = std::make_unique<C>();
    }
}

However, note that when owning pointers from an abstract base class, you will always want to have a virtual destructor -- otherwise you may incur a memory leak:

class A {
public:
    ...
    virtual ~A() = default;
};

You can also do something similar with references if you don't want to use heap memory -- at which point the semantics will change a little bit.

References in C++ can only refer to an object that already has a lifetime (e.g. has been constructed), and can't refer to a temporary. This means that you'd have to have instances of B and C to choose from, such as:

int main() {
    auto b = B{};
    auto c = C{};
    bool q = getUserInput();

    A& a = q ? b : c;
    a.print(); // A& references either 'b' or 'c'
}
Human-Compiler
  • 11,022
  • 1
  • 32
  • 59
  • 1
    Re: “otherwise you may encounter a memory leak” — maybe. The behavior of the program is undefined. Also, there are no casts involved, so “upcast” is misleading; it’s really about implicit conversions. +1. – Pete Becker Apr 20 '21 at 21:25
0

You need to use pointers :

A* a = q ? static_cast<A*>(new B) : new C;
... 
delete a;

A ternary is also a special case here, as it takes the type of the first expression.

If C inherited from B here it would not be a problem.

Robert Andrzejuk
  • 5,076
  • 2
  • 22
  • 31
  • 1
    There's a bigger problem in OP's code: they use `private` inheritance. This will still not work until the inheritance is `public` – Human-Compiler Apr 20 '21 at 20:19
  • There is no need for that cast. `new B` and `new C` can both be implicitly converted to `A*`, so that’s the type of the ternary expression (assuming the private inheritance problem is fixed). – Pete Becker Apr 20 '21 at 21:32