3

I have a cross platform C++ application build with C++ Builder 10.1 Berlin and have a problem understanding the lifetime handling of objects, in this case strings, wich are declared outside the class. I have created a new forms application and added some code. The cpp file looks like this:

#include
#pragma hdrstop
#include "FmrMain.h"
#pragma package(smart_init)
#pragma resource "*.fmx"
TForm1 *Form1;

const String Hello = "Hello";

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
   ShowMessage(Hello);
}

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
   ShowMessage(Hello);
}

I compile this with the CLANG enhanced C++11 compiler bcc32c, run the application and close the form again. When TForm1::FormDestroy is called Hello is allready destroyed. When I compile the code for win32 with the classic compiler bcc32 the string is destroyed after FormDestroy.

Can someone explain this or provide some information about the topics I have to look for? Why is the CLANG based compiler behaving different here?

Edit

It's easier to debug when I use a self defined class instead of a string.

class Foo {
public:
   Foo(){};
  ~Foo(){}
};
Foo A;

//--------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//--------------------------------------------------------------------------

__fastcall TForm1::~TForm1()
{
}

The creation and destruction oder is like this. I have added the call stacks.

bcc32c (CLANG C++11 compiler)

  1. create Foo

    :004052C0 Foo(this=:00400000)
    :00405070 __cxx_global_var_init3()
    :004052A3 _GLOBAL__I_a()
    :00405ab7 ; ~Foo
    :321fa2b7 ; C:\Program Files (x86)\Embarcadero\Studio\18.0\bin\CC32C240MT.DLL
    :321fa6ff CC32C240MT.__wstartup + 0xbb

  2. create Form1

    :004052EC TForm1(this=:00402422, __ctor_flag='\0')
    :0085c139 fmx240.@Fmx@Forms@TApplication@CreateForm$qqrxp17System@TMetaClasspv + 0x5d
    :0085c349 fmx240.@Fmx@Forms@TApplication@RealCreateForms$qqrv + 0x81

  3. destroy Foo

    :004052D0 ~Foo(this=:0040B7DC)
    :0040509E __dtor_A()
    :321f6246 CC32C240MT.___call_atexit_procs + 0x52
    :321f671c CC32C240MT.___exit + 0x20

  4. destroy Form1

    :00405868 ~TForm1(this=:5016E698)

bcc32 (Classic borland compiler)

  1. create Foo

    :00404950 Foo::Foo(this=:00409B74)
    :004048A0 STCON0()
    :00405727 ; IRoot
    :322190f1 ; C:\Program Files (x86)\Embarcadero\Studio\18.0\bin\CC32240MT.DLL > :322193b5 CC32240MT.__wstartup + 0xa5

  2. create Form1

    :00404994 TForm1::TForm1(this=:02F2AE20, Owner=:02F39620)
    :0095c139 fmx240.@Fmx@Forms@TApplication@CreateForm$qqrxp17System@TMetaClasspv + 0x5d
    :0095c349 fmx240.@Fmx@Forms@TApplication@RealCreateForms$qqrv + 0x81

  3. destroy Form1

    :00404ABC TForm1::~TForm1(this=:02F2AE20)

  4. destroy Foo

    :00404978 Foo::~Foo(this=:00409B74)
    :0040493F STDES0()
    :0040573f ;IRoot>
    :3221910f ; C:\Program Files(x86)\Embarcadero\Studio\18.0\bin\CC32240MT.DLL
    :3221915b ; C:\Program Files (x86)\Embarcadero\Studio\18.0\bin\CC32240MT.DLL > :3221944a ; C:\Program Files (x86)\Embarcadero\Studio\18.0\bin\CC32240MT.DLL

Kerem
  • 429
  • 5
  • 20
  • The `const String Hello` is a const object. It should not be destroyed before the main function returned. Probably It has never been created and the compiler has created a string constant instead that was sufficient as the argument to the `ShowMessage` function. You can use a debugger to check this. – harper Jun 13 '16 at 09:12
  • When I check the variable `Hello` it is `NULL` when the `FormDestroy`is called. – Kerem Jun 13 '16 at 09:48
  • Did you also check the parameter value passed to ShowMessage()? If that was correct then it confirms my assumption. – harper Jun 13 '16 at 10:03
  • 1
    Your example with a self-defined class is worth to set breakpoints in the destructors. Look at the callstack at Form1 destructor. I suspect that Form1 is not created on hesp but as a file scope veriable. In that case you have an implementation defined order of calls to constructurs and destructors. This leads to undefined behavior of your program. – harper Jun 13 '16 at 10:16
  • Hmm I don't know if i understand you right. `ShowMessage` is a system function and I can't debug it. I only could set a breakpoint at the line where `ShowMessage` is called – Kerem Jun 13 '16 at 10:17
  • 2
    If ShowMessage is a system function then you have to switch to disassembly view. Use the ABI specification how arguments are passed to the function an look at the memory dump. So you can see what is passed to the function. – harper Jun 13 '16 at 10:21
  • I don't know where I can find the ABI spec but I have looked at the header file where `ShowMessage` is defined. The string is passed by value. The other thing is that I don't know how the memory dump is accessible. I'm also not familiar with debuging in the CPU view... – Kerem Jun 13 '16 at 11:09
  • How can I find out if `Form1` was created on heap or a a file scope? – Kerem Jun 13 '16 at 11:11
  • Evaluate the callstack from constructor. You should find where and how the object ist created. – harper Jun 13 '16 at 11:15
  • 2
    What is causing these things to be destroyed? Is it explicit program code, or are they being destroyed as part of program termination? In the latter case, I don't think C++ provides any guarantees as to the order in which statically allocated objects are destroyed. – nugae Jun 13 '16 at 11:23
  • The objetcs get destroyed at program termination. – Kerem Jun 13 '16 at 11:30
  • 2
    @KeremD: `TObject`-derived classes, including `TForm`, are **always** allocated on the heap. In your example, the `String` is in global memory and is being deallocated by the compiler before the RTL destroys the global `TApplication` object that owns your `TForm1` object. IOW, your `TForm1` is outliving your `String`, that is why it is NULL in your `OnDestroy` event handler (which BTW, you should NEVER BE USING IN C++! Use the `~TForm1` destructor instead). – Remy Lebeau Jun 13 '16 at 21:26

1 Answers1

5

Auto-created TForm objects are owned by the global TApplication object. That object is destroyed (thus destroying its owned Forms) after the application's main()/wmain()/WinMain() entry point function has exited. Globals are destroyed during application cleanup.

The lifetime of your global String is not guaranteed to outlive the lifetime of the global TApplication object, in either compiler. You are relying on undefined behavior based on the cleanup order of globals across different units. And worse, you are relying on the cleanup order across different frameworks! Your String is in your own C++ code, but the TApplication object is in the Delphi-based RTL library.

If your String needs to remain alive for the lifetime of the TForm that uses it, you should declare it as a static member of that class instead:

FmrMain.h:

//---------------------------------------------------------------------------

#ifndef FmrMainH
#define FmrMainH
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <FMX.Controls.hpp>
#include <FMX.Forms.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:    // IDE-managed Components
private:    // User declarations
    static const String Hello;
public:     // User declarations
    __fastcall TForm1(TComponent* Owner);
    __fastcall ~TForm1();
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

FmrMain.cpp:

//---------------------------------------------------------------------------
#include <fmx.h>
#pragma hdrstop

#include "FmrMain.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.fmx"
TForm1 *Form1;

const String TForm1::Hello = "Hello";
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
    ShowMessage(Hello);
}

void __fastcall TForm1::~TForm1()
{
    ShowMessage(Hello);
}
//---------------------------------------------------------------------------

Alternatively, use a wchar_t* instead of a String, then you don't run into any cleanup issues:

//---------------------------------------------------------------------------
#include <fmx.h>
#pragma hdrstop

#include "FmrMain.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.fmx"
TForm1 *Form1;

static const wchar_t* Hello = L"Hello";
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
    ShowMessage(Hello);
}

void __fastcall TForm1::~TForm1()
{
    ShowMessage(Hello);
}
//---------------------------------------------------------------------------
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • @RemyLebau: Thx for this nice answer. I was allready afraid of this, since we use this technique to define constant strings for a long time... I also tried to define the string as a static class member in the same way you suggest. Also in this case `Hello` gets gestroyed before `Form1` and is `NULL` when the destructor of `Form1` is called. This was so frustrating that I defined the strings as `wchar_t*` and asked the question here. – Kerem Jun 14 '16 at 06:50
  • I looked at [What is the lifetime of a static variable in a C++ function?](http://stackoverflow.com/questions/246564/what-is-the-lifetime-of-a-static-variable-in-a-c-function) and [What is the lifetime of class static variables in C++?](http://stackoverflow.com/questions/1079623/what-is-the-lifetime-of-class-static-variables-in-c). In [class.static.data] the C++ standard says: "Static data members are initialized and destroyed exactly like non-local variables". I think that the static member `TForm1::Hello` is also constructed and distroyed like a static variable defined outside the class. – Kerem Jun 29 '16 at 07:24