11

Inspired by this question, I tried the following code:

struct A {
  virtual void doit() const = 0;
};

struct B : public A {
  virtual void doit() const;
};

struct C : public A {
  virtual void doit() const;
};

void
foo(bool p)
{
  const A &a = (p ? static_cast<const A &>(B()) : static_cast<const A &>(C()));
  a.doit();
}

Every compiler I have tried accepts this code with -Wall -Werror and generates the assembly I want. But after carefully reading the C++03 specification section 12.2 ("Temporaries") and section 5.12 ("Conditional Operator"), I am unsure whether this is guaranteed to work.

So, is this valid code, or does it invoke undefined behavior? Does the answer differ for C++03 and C++11?

Citations from relevant specifications would be appreciated.

Community
  • 1
  • 1
Nemo
  • 70,042
  • 10
  • 116
  • 153
  • http://stackoverflow.com/questions/14405837/lifetime-extension-and-the-conditional-operator – M.M Apr 14 '14 at 00:23
  • 1
    http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#86 – M.M Apr 14 '14 at 00:23
  • Looks perfectly fine to me... What, specifically, is your concern? – ildjarn Apr 14 '14 at 00:26
  • 4
    Also relevant: http://www.open-std.org/JTC1/SC22/WG21/docs/cwg_closed.html#1568 I think http://www.open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1376 suggests that you get a dangling reference due to the cast. – dyp Apr 14 '14 at 00:27
  • @ildjarn: Both dangling references and "slicing" seem like things that could go wrong, at least in principle. – Nemo Apr 14 '14 at 00:46
  • @MattMcNabb: What answer do you think those imply to my question? The CWG reference seems particularly apt -- it actually mentions this specific case -- but I do not understand what they say the answer is. I also do not know what language in the C++11 spec applies here. – Nemo Apr 14 '14 at 00:47
  • There's no slicing, because you're binding the object to a reference. And there are no dangling references, because you are extending the lifetime of a temporary by binding it to a _const_ reference. :-] – ildjarn Apr 14 '14 at 00:58
  • @ildjarn: Re: slicing. Are you certain no copy of type `A` is being made (see Matt McNabb's link)? Re: dangling reference. Did you follow and read dyp's link? I do not think this is a trivial question at all. If you disagree, feel free to answer and cite the relevant section(s) of the spec... – Nemo Apr 14 '14 at 01:04
  • Don't know... if the WG isn't sure then I'm even less sure :) – M.M Apr 14 '14 at 01:18
  • @ildjarn: Also, see Alf's answer, which shows that either you are wrong or GCC has a bug :-) – Nemo Apr 14 '14 at 01:22
  • @Nemo I don't think there's slicing in your example, since `static_cast(B())` is an lvalue (so there's no lvalue-to-rvalue conversion as in CWG86). The implementation of `?:` for reference arguments can be as simple as choosing one of two addresses to initialize a pointer. – dyp Apr 14 '14 at 15:21
  • @dyp: I agree there is no slicing; just pointed it out as something that "might go wrong". The actual problem here is temporary lifetime / dangling references. Note that you do get slicing, I think, if you make `A` concrete and do `const A & a = (p ? A() : B());` -- or at least, that is what the behavior of GCC 4.8.1 seems to show. I am still waiting/hoping to see someone cite chapter and verse of the spec. – Nemo Apr 14 '14 at 18:22
  • I don't think `A const& a = p ? A() : B();` has a lifetime issue. It creates a temporary `A` from either `A()` or `B()` and binds this temporary to the reference, extending its lifetime (this is what CWG86 is about). – dyp Apr 14 '14 at 18:28

1 Answers1

6

Oh, it's very invalid.

Consider:

#include <iostream>
using namespace std;

struct A {
    virtual ~A() { cout << "~A" << endl; }
    virtual void doit() const = 0;
};

struct B : public A
{
    ~B() override { cout << "~B" << endl; }
    void doit() const override { cout << "A::doit" << endl; }
};

struct C : public A
{
    ~C() override { cout << "~C" << endl; }
    virtual void doit() const { cout << "C::doit" << endl; }
};

void foo(bool p)
{
    cout << "foo( " << p << ")" << endl;
    const A &a = (p ? static_cast<const A &>(B()) : static_cast<const A &>(C()));
    a.doit();
}

auto main( int argc, char* argv[] ) -> int
{
    cout << boolalpha;

    foo( true );
    cout << endl;
    foo( false );
}

Output in Coliru Viewer, using g++ 4.8:

foo( true)

~B

~A

pure virtual method called

terminate called without an active exception

bash: line 7: 16922 Aborted                 (core dumped) ./a.out

It's UB so any explanation could be true, but one can be reasonably sure, without looking at the assembly, that what happens is:

  • A temporary is constructed.
  • It's bound to the reference.
    This is a reference being bound to a reference, so does not involve creation of a new temporary or slice.
  • The temporary is destroyed.
  • As part of that its dynamic type (vtable pointer) is changed to A, which is abstract.
  • The pure virtual in A is called.
Cheers and hth. - Alf
  • 142,714
  • 15
  • 209
  • 331
  • Oh, sorry for imperfect code, I see that I didn't manage to change declarations to `override` everywhere. And re the standardeese, it's night here in Norway, and I need some rest before getting up in a few hours. Howevever, perhaps answer should be extended with some clarifying note that the question is *not* lifetime extension. – Cheers and hth. - Alf Apr 14 '14 at 01:14
  • Without going into discussion how awful code in question is, why isn't temporary bound to the const reference? Shouldn't it extend the lifetime until end of the scope? I think that is what OP expected. – BЈовић Apr 14 '14 at 07:27
  • 1
    @BЈовић: as noted by dyp in question comments there is an as yet unresolved defect report concerning the isse, **[DR 1376](http://www.open-std.org/JTC1/SC22/WG21/docs/cwg_defects.html#1376)**. The problem there is that the effect of a `static_cast` to reference is defined in terms of an invented "temporary" object. But that's just a wording problem. The lifetime extension only applies to direct binding of a temporary. Binding a reference to a reference does not extend lifetime, essentially because defining it properly (e.g. disallowing reference produced by function call) would be complicated. – Cheers and hth. - Alf Apr 14 '14 at 11:09
  • 1
    +1, although how one compiler happens to behave does not fully resolve the question. I will wait to see if anyone cares to cite chapter and verse of the spec; if nobody does that in the next few days, I will accept this answer. – Nemo Apr 14 '14 at 18:25