3

The following code compiles cleanly, using g++ 6.3.0 with -Wall.

#include <iostream>

class Base
{
public:
    Base(char const* base) : base_(base) {}
    void print( char const* msg ) { print( base_, msg ); }

protected:
    ~Base() = default;

private:
    char const* base_;
    virtual void print( char const*, char const* ) = 0;
};

class Drv1 : public Base
{
public:
    Drv1(char const* base, int i) : Base(base) , i_(i) {}
    ~Drv1() { std::cout << "Drv1 dtor" << std::endl; }

private:
    int     i_;

    void print( char const* base, char const* msg ) override
    {
        std::cout << base << "(" << msg << "): " << i_ << std::endl;
    }
};

class Drv2 : public Base
{
public:
    Drv2(char const* base, double d) : Base(base) , d_(d) {}
    ~Drv2() { std::cout << "Drv2 dtor" << std::endl; }

private:
    double     d_;

    void print( char const* base, char const* msg ) override
    {
        std::cout << base << "(" << msg << "): " << d_ << std::endl;
    }
};


void do_test( char const* base, char const* msg, bool int_type )
{
    Base&&   _base(int_type ? (Base&&)Drv1(base, 1) : (Base&&)Drv2(base, 2.5));

    _base.print( msg );
}

int main()
{
    do_test( "Test1", "int", true );
    do_test( "Test2", "double", false );
    return 0;
} 

The output, when run, is this:

Drv1 dtor
Test1(int): 1
Drv2 dtor
Test2(double): 2.5

Questions:

  1. How can this be defined behavior if the derived class destructors have been invoked before the calls to the virtual functions? If the output is actually just a lucky accident, what are the compiler options to catch this issue?

  2. Is rvalue reference the correct terminology for the type of the local variable _base in do_test()? Universal (or forwarding) references occur in the context of templates, but there are no templates here.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
arayq2
  • 2,502
  • 17
  • 21

1 Answers1

3

Lifetime extension of a temporary by binding a reference to it has same rules for rvalue references as they are for lvalue references - except that non-const rvalue references can be bound to temporaries while non-const lvalue references cannot.

The program does have UB. In this expression: (Base&&)Drv2(base, 2.5) a temporary object is constructed, and a reference is bound to it. Next, the reference is used to initialise another reference in the full expression Base&& _base(int_type ? (Base&&)Drv1(base, 1) : (Base&&)Drv2(base, 2.5));. The lifetime of the temporary referred by the temporary reference is not extended by the lifetime of _base. Thus the reference is left dangling. Later accessing the value has undefined behaviour.

Extending lifetime of a temporary works only when initialising the reference directly with the expression that creates the temporary. For example:

Base&&   _base(Drv2(base, 2.5));
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • If extended temporary lifetimes can't be "passed on" like this, then isn't the automatic dangling reference detectable by static analysis and thus by the compiler? There should be a warning for this particular case. – arayq2 Feb 10 '19 at 02:51