dimanche 29 janvier 2017

C++11 scoping and lifetime of temporary bound to a (const) reference (GCC)

I have the following questions related to the same situation (not in general):

  • Why does the compiler not produce a warning when a temporary is bound to a reference?
  • How does lifetime extension of a temporary work (when it is bound to a reference)?
  • How to interpret / understand the C++ standard (C++11)?
  • Is this a bug (in the compiler)? Should it be?

So this is what we are talking about:

struct TestRefInt {
    TestRefInt(const int& a) : a_(a) {};
    void DoSomething() {    cout << "int:" << a_ << endl;  }
protected:
    const int& a_;
};

Should TestRefInt tfi(55); and tfi.DoSomething(); work? How and where? So here is a code

TestRefInt tfi(55);

int main() {
    TestRefInt ltfi(8);

    tfi.DoSomething();
    ltfi.DoSomething();
    return 0;
}

What should this do?

Here I will elaborate some more.

Consider this real world (simplified) example. How does it look like to a novice/beginner C++ programmer? Does this make sense?

(you can skip the code the issue is the same as above)

#include <iostream>
using namespace std;

class TestPin //: private NonCopyable
{
    public:
        constexpr TestPin(int pin) : pin_(pin) {}
        inline void Flip()   const {
            cout << " I'm flipping out man : " << pin_ << endl;
        }
    protected:
        const int pin_;
};

class TestRef {
public:
    TestRef(const TestPin& a) : a_(a) {};
    void DoSomething() {    a_.Flip();  }
protected:
    const TestPin& a_;
};

TestRef g_tf(1); 

int main() {
    TestRef tf(2);

    g_tf.DoSomething();
    tf.DoSomething();

    return 0;
}

Command line:

/** Compile:
Info: Internal Builder is used for build
g++ -std=c++11 -O0 -g3 -Wall -Wextra -Wconversion -c -fmessage-length=0 -o "src\\Scoping.o" "..\\src\\Scoping.cpp" 
g++ -o Scoping.exe "src\\Scoping.o" 
13:21:39 Build Finished (took 346ms)
 */

Output:

/** 
 I'm flipping out man : 2293248
 I'm flipping out man : 2
 */

The issue: TestRef g_tf(1); /// shouldn't the reference to temporary extend it's life in global scope too?

What does the standard says?

From How would auto&& extend the life-time of the temporary object?

Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself

  ^^^^^

Globals are not allocated on stack, so lifetime is not extended. However, the compiler does not produce any warning message whatsoever! Shouldn’t it at least do that?

But my main point is: from a usability standpoint (meaning as the user of C++, GCC as a programmer) it would be useful if the same principle would be extended to not just stack, but to global scope, extending the lifetime (making global temporaries “permanent”).

Sidenote: The problem is further complicated by the fact that the TestRef g_tf(1); is really TestRef g_tf{ TestPin{1} }; but the temporary object is not visible in the code, and without looking at the constructor declaration it looks like the constructor is called by an integer, and that rarely produces this kind of error.

As far as I know the only way to fix the problem is to disallow temporaries in initialization by deleting TestRefInt(const int&& a) =delete; constructor. But this also disallows it on stack, where lifetime extension worked.

But the previous quote is not exactly what the C++11 standard say. The relevant parts of the C++11 Standard are 12.2 p4 and p5:

4 - There are two contexts in which temporaries are destroyed at a different point than the end of the full expression. The first context is [...]

5 - The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

— A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

— A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call. § 12.2 245 c ISO/IEC N3337

— The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.

— A temporary bound to a reference in a new-initializer (5.3.4) persists until the completion of the full-expression containing the new-initializer. [Example: struct S { int mi; const std::pair& mp; }; S a { 1, {2,3} }; S* p = new S{ 1, {2,3} }; // Creates dangling reference — end example ] [ Note: This may introduce a dangling reference, and implementations are encouraged to issue a warning in such a case. — end note ]

My English and understanding of the standard is not good enough, so what exception is this? The “— A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.” or “reference parameter in a function call” does not mention anything about allocation on stack or in global scope. The other exceptions do not seem to apply. Am I wrong?

Which one is this, or none of them?

We have a temporary in a function call (the constructor) and then a reference to the temporary is bound to a member reference in the initializer. Is this undefined behavior? Or the exception still applies, and the temporary should be destroyed? (or both)

What about this?

struct TestRefIntDirect {
    TestRefIntDirect(int a) : a_(a) {};
    void DoSomething() {    cout << "int:" << a_ << endl;  }
protected:
    const int& a_;
};

One less reference, same behavior.

Why does it work in one case (instantiation inside a function) versus in other case (in global scope)?

Does GCC not “destroy” one of them by “accident”?

My understanding is that none of them should work, the temporary should not persist as the standard says. It seems GCC just "lets" you access not persisting objects (sometimes). I guess that the standard does not specify what the compiler should warn about, but can we agree that it should? (in other cases it does warn about ‘returning reference to temporary’) I think it should here too.

Is this a bug or maybe there should be a feature request somewhere?

It seems to me like GCC says “Well, you shouldn’t touch this cookie, but I will leave it here for you and not warn anyone about it.” ...which I don’t like.

This thing others reference about stack I did not find in the standard, where is it coming from? Is this misinformation about the standard? Or is this a consequence of something in the standard? (Or is this just implementation coincidence, that the temporary is not overwritten and can be referenced, because it happened to be on the stack? Or is this compiler defined behavior, extension?)

The more I know C++ the more it seems that every day it is handing me a gun to shoot myself in the foot with it…

I would like to be warned by the compiler if I’m doing something wrong, so I can fix it. Is that too much to ask?

Other related posts:

Temporary lifetime extension

C++: constant reference to temporary

Does a const reference prolong the life of a temporary?

Returning temporary object and binding to const reference

I didn’t want to write this much. If you read it all, thanks.

Aucun commentaire:

Enregistrer un commentaire