1

After encountering a similar situation in a real-world application I decided to put together a demo which shows that if I store derived classes as a pointer to the base class, and call a virtual method the behavior of the derived class will be incorrect. See the code below:

struct IntWrapper { int value; };

struct Base {
    virtual ~Base() = default;
    virtual void myMethod() = 0;
};

struct Foo : public Base {

    const IntWrapper& x;

    Foo(const IntWrapper& x) : x(x) {}

    void myMethod() override {
        std::cout << std::to_string(x.value) << std::endl;
    }

};

struct Bar : public Base {

    const IntWrapper& x;
    const IntWrapper& y;

    Bar(const IntWrapper& x, const IntWrapper& y) : x(x), y(y) {}

    void myMethod() override {
        std::cout << std::to_string(x.value) << " " << std::to_string(y.value) << std::endl;
    }

};

int main()
{
    Base* foo = new Foo(IntWrapper{3});
    Base* bar = new Bar(IntWrapper{5}, IntWrapper{42});
    foo->myMethod();
    bar->myMethod();
    return 0;
}

The expected output would be:

3
5 42

Instead I receive:

42
5 42

Interestingly, if I replace IntWrapper reference with a primitive int in the classes Foo and Bar the printed values will be correct. Can somebody explain to me why this behavior happens?

  • 6
    All of your member references are dangling, hence you have UB. – Passer By Aug 20 '18 at 09:41
  • 3
    See https://stackoverflow.com/questions/2784262/does-a-const-reference-class-member-prolong-the-life-of-a-temporary and decide if it fully answers your problem (aka a duplicate). – Passer By Aug 20 '18 at 09:42
  • 1
    `Base* foo = new Foo(IntWrapper{3});` creates a temporary `IntWrapper` (destroyed at the end of the full expression); you save a reference to this temporary in `foo`. When you next use `foo` the saved reference is dangling (as the temporary has been destroyed) and you have Undefined Behaviour. – Richard Critten Aug 20 '18 at 09:44
  • Just delete the `const` and references. – macroland Aug 20 '18 at 09:53
  • Useful rule of thumb: when you think you want reference members, you're not done with the thinking yet. – molbdnilo Aug 20 '18 at 09:58

2 Answers2

3
Base* foo = new Foo(IntWrapper{3});
Base* bar = new Bar(IntWrapper{5}, IntWrapper{42});

You're creating an IntWrapper temporary that you're passing as a reference to the constructors, that save that reference for later use. As the temporary gets destructed after the constructor, your references inside foo and bar are invalid, and you're invoking undefined behavior, meaning anyhting can happen.

You need to either create IntWrapper variables that you will pass to the constructors, like so:

IntWrapper x{3};
IntWrapper y{5};
IntWrapper z{42};
Base* foo = new Foo(x);
Base* bar = new Bar(y,z);

Or your classes should make copies of passed IntWrappers, instead of holding a reference to them.

Kaldrr
  • 2,780
  • 8
  • 11
  • Okay, so what caused my confusion is that I thought const reference class members prolong the lifetime of temporaries, but it seems that they don't. Passer By's comment and your answer fully sums up the answer, thanks. – Krisztián Szabó Aug 20 '18 at 10:03
1

All of your member references are dangling, hence you have UB. – Passer By

and

Base* foo = new Foo(IntWrapper{3}); creates a temporary IntWrapper (destroyed at the end of the full expression); you save a reference to this temporary in foo. When you next use foo the saved reference is dangling (as the temporary has been destroyed) and you have Undefined Behaviour. - Richard Critten

I added a little bit printf debugging to the sample code of OP for demonstration. Now, the effects described in comments become quite obvious (IMHO):

#include <iostream>

struct IntWrapper {
  int value;
  IntWrapper(int value): value(value)
  { 
    std::cout << "IntWrapper::IntWrapper(" << value << ")\n";
  }
  ~IntWrapper()
  { 
    std::cout << "IntWrapper::~IntWrapper(" << value << ")\n";
  }
};

struct Base {
    Base(const char *text)
    {
      std::cout << text << "::" << text << "()\n";
    }
    virtual ~Base() = default;
    virtual void myMethod() = 0;
};

struct Foo : public Base {

    const IntWrapper& x;

    Foo(const IntWrapper& x): Base("Foo"), x(x) { }

    void myMethod() override {
        std::cout << std::to_string(x.value) << std::endl;
    }

};

struct Bar : public Base {

    const IntWrapper& x;
    const IntWrapper& y;

    Bar(const IntWrapper& x, const IntWrapper& y): Base("Bar"), x(x), y(y) {}

    void myMethod() override {
        std::cout << std::to_string(x.value) << " " << std::to_string(y.value) << std::endl;
    }

};

int main()
{
    Base* foo = new Foo(IntWrapper{3});
    Base* bar = new Bar(IntWrapper{5}, IntWrapper{42});
    std::cout << "foo->myMethod();\n";
    foo->myMethod();
    std::cout << "bar->myMethod();\n";
    bar->myMethod();
    return 0;
}

Output:

IntWrapper::IntWrapper(3)
Foo::Foo()
IntWrapper::~IntWrapper(3)
IntWrapper::IntWrapper(5)
IntWrapper::IntWrapper(42)
Bar::Bar()
IntWrapper::~IntWrapper(42)
IntWrapper::~IntWrapper(5)
foo->myMethod();
3
bar->myMethod();
5 42

Live Demo on coliru

Note:

The fact that the sample matched the expected output should not be mis-interpreted. It's still undefined behavior.

Scheff's Cat
  • 19,528
  • 6
  • 28
  • 56