15 мая 2023 года "Исходники.РУ" отмечают своё 23-летие!
Поздравляем всех причастных и неравнодушных с этим событием!
И огромное спасибо всем, кто был и остаётся с нами все эти годы!

Главная Форум Журнал Wiki DRKB Discuz!ML Помощь проекту


Memory leak in CFile constructor.

Giles R. Chamberlin -- GRC@langley.softwright.co.uk
Friday, August 16, 1996


Environment: VC+4.1, MFC4.1, Win 95, Win NT4 beta2

SUMMARY
One of the CFile constuctors throws an exception without cleaning up,   
causing a memory leak.

TEST SETUP:
AppWizard generated dialog box, add a button to the dialog,add an on   
click handler for the button as follows:

void CBugDlg::OnMsgbox()
{
 UINT flags = CFile::modeRead | CFile::shareDenyNone;
 CString strFile = "NoFile.txt";  // This file does not exist
 
 try
 {
  CFile* pFile = new CFile(strFile, flags); // Constructor throws an   
exception
       // Assignment to pFile never happens.
  delete pFile;    // Never get here.
 }
 catch(CFileException e)
 {
  TRACE("Caught an Exception")
 }
}

The constructor throws an exception and pFile is never assigned. On   
quitting the program, a 16 byte CObject memory leak is detected.   
 _CrtSetBreakAlloc() confirms that this is the object created by the new   
CFile.

PROBLEM CODE:
In MFC\SRC\Filecore.cpp we have:

CFile::CFile(LPCTSTR lpszFileName, UINT nOpenFlags)
{
 ASSERT(AfxIsValidString(lpszFileName));

 m_strFileName.Empty();
 CFileException e;
 if (!Open(lpszFileName, nOpenFlags, &e))
  AfxThrowFileException(e.m_cause, e.m_lOsError, e.m_strFileName);
}

WORK AROUND:
To experiment, I created a similar class, derived directly from CObject   
and called its constructor in a similar test frame as the CFile case   
above.

Noddy::Noddy()
{
 TRACE("Constructor\n");
 delete this; // Without this we get a memory leak as per CFile.
 throw i;
}

Noddy::~Noddy()
{
 TRACE("Destructor\n");
}

QUESTION:
The addition of "delete this"  prior to throwing the exception solves the   
memory leak problem, but is concerning me slightly as ~Noddy() is called   
and I don't yet have a valid object.  So, should the MFC CFile::CFile be   
calling delete this before throwing the exception?  Or should I be   
handling things differently in my calling routine.?
Any comments?

Giles Chamberlin
Softwright






Mike Blaszczak -- mikeblas@nwlink.com
Sunday, August 18, 1996

[Mini-digest: 8 responses]

At 09:33 AM 8/16/96 PDT, you wrote:
>Environment: VC+4.1, MFC4.1, Win 95, Win NT4 beta2

>void CBugDlg::OnMsgbox()
>{
> UINT flags = CFile::modeRead | CFile::shareDenyNone;
> CString strFile = "NoFile.txt";  // This file does not exist
> 
> try
> {
>  CFile* pFile = new CFile(strFile, flags); // Constructor throws an   
>exception
>       // Assignment to pFile never happens.
>  delete pFile;    // Never get here.
> }
> catch(CFileException e)
> {
>  TRACE("Caught an Exception")
> }
>}

Your code has two flaws:

> catch(CFileException e)

First, you should be catching "CFileException* e", not "CFileException e".

> {
>  TRACE("Caught an Exception")
> }

Second, you should be coding (now that you've fixed the first problem):
       
   delete e->Delete();

If you fix these flaws and still have a problem, please write us back.

.B ekiM
http://www.nwlink.com/~mikeblas   <--- trip report central
1995 Honda VFR750F (Serial number 00050!)  4 Corners 1996!
1987 Yamaha FZ700 (damaged)                AMA, HRC, VFROC
     These words are my own: I do not speak for Microsoft.

-----From: Jon Shadforth 

*** SNIP ***

The leak is not a problem with the CFile constructor. It occurs because you are 
dynamically creating a CFile instance which can only be cleaned up correctly 
with a call to delete. But, if an exception is thrown by the CFile constructor 
then your 'delete pFile' code will not get called. There are two 
simple workarounds to this problem:
1. Create the CFile instances locally on the stack - as soon as the instance 
looses scope it will be destructed automatically.
2. Declare pFile before the try block and initialise it to NULL, and also 
re-NULL it once you call delete. Then, during the exception handler you can test 
to see if the pointer was created and delete it. 

Also, I would never call 'delete this' from within a constructor, or from 
within any other class member - conceptually it is too wierd to try and figure 
out, especially since the object could have been created automatically on the 
stack!!!

Jon :-)

-- 
---------------------------------------------------------------------------
| Jon Shadforth                            EMail jon@egg-nogg.demon.co.uk |
---------------------------------------------------------------------------

-----From: Mario Contestabile

That can't be true. 
C++ only destroys fully constructed 
objects, and an object isn't fully constructed until its constructor has 
run completion. If an exception is thrown during b's construction, b's
destructor will never be called. C++ won't clean up after objects that throw
exceptions during construction. You must design your constructors such that
they clean up after themselves.

It would be cleaner to simply have a m_CFile, and avoiding the calls to new and 
delete 
by creating the object, class local.
    m_CFile(strFile, flags);

In any case, using the Microsoft try-catch convention

TRY{
      CFile* pFile = new CFile(strFile, flags); // Constructor throws an  
exception
       // Assignment to pFile never happens.
      delete pFile;    // Never get here.
}
CATCH(CFileException, e){
     TRACE("Caught an Exception");
}
END_CATCH

will eliminate your leak.

mcontest@universal.com

-----From: Wolfgang Loch 

Hi Giles.

You address the same problem we discussed recently on mfc-l.
"exception in constructor results in memory leak"
You may want to read Mike B.'s comment on this subject. Although 
he said the problem should be fixed in VC4.1.

Calling "delete this" in the constructor is definitely not a
good idea. Think about what would happen if the object is not
constructed on the heap. You would try to free memory from
the stack. And maybe the destructor will be called twice.

A Solution for your special CFile problem should be:

CFile* pFile = new CFile(); // no exception, no problem
CFileException e;
if( !pFile->Open(strFile, flags, &e) )
{
  delete pFile;
  TRACE("File not Opened")
}

No pretty exception handling. But it works.

I made the same experience, that the object's destructor is
being called although no valid object could be created. Nobody 
seamed to believe me.
I guess the c++ language standard says the destructor should
never be called if the constructor is not finished successfully.
But what happens when I do this:

CMyClass::CMyClass() {
  m_pInt = new int[10];
  if( !DoCriticalThing() )
    throw new CException;
}
CMyClass::~CMyClass() {
  delete m_pInt
}

Here the destructor should be invoked independend of the
the exception being thrown or not.

Wolfgang
-- 
/-------------------------------------------------\
| Wolgang Loch  (Technical University of Ilmenau) |
|   e-mail: Wolfgang.Loch@rz.TU-Ilmenenau.DE      |
|   www   : http://www.rz.tu-ilmenau.de/~wolo     |
\-------------------------------------------------/
-----From: "Giles R. Chamberlin" 


Mario,
Thanks for your answer, but I must have explained myself badly -

 > If an exception is thrown during b's construction, b's
 > destructor will never be called. C++ won't clean up after objects that   
throw
 > exceptions during construction.
I agree with you here.

 > You must design your constructors such that
 > they clean up after themselves.
And agree with this.  Unfortunately the CFile constructor in question   
(from MFC, not mine) does not clean up.

 > It would be cleaner to simply have a m_CFile, and avoiding the calls
 > to new and delete by creating the object, class local.
Possibly so, but more generally if the new operator is accessible for a   
given class, then it should be allowable to call it.  This isn't an   
attack on MFC - if I am right and there is a bug in this constructor, big   
deal.  The reason I'm pursuing this is that I don't fully understand   
exceptions and am trying to ascertain what should happen and why.

 > In any case, using the Microsoft try-catch convention
 > TRY{...} CATCH(CFileException, e){...}END_CATCH
 > will eliminate your leak.
'Fraid not, the problem remains exactly the same.  As an aside, according   
to the online docs, the TRY/CATCH macros are no longer the method of   
choice.  Now that C++ style try/catch is available,  this is the method   
of choice (See "Exceptions" in "Programming with MFC" in the online   
help).

Thanks,
Giles Chamberlin
Softwright



-----From: Pradeep Tapadiya 

ANSWER:

NEVER call "delete this" from a constructor.

1) It may call the destructor for a partially constructed object.

2) More importantly, you are freeing a memory location twice - once
   by delete and once by implicit exception handling mechanism.

Please look at the mfc-l archive (is there one?) for a mail I sent
out two weeks ago. Basically, there is a bug in the way Microsoft
has implemented their heap cleanup when a constructor having an
overloaded new operator throws an error. The work around is just to
make sure that the constructor can never throw an exception. In your
case, perhaps you can look at what exception is being thrown and handle
this case even before you do a "new CFile."

By the way, the leak that you see will happen only in the debug mode
but not in the release mode. Operator new is overloaded under debug
mode so that it can store file/line information.

Hope this helps.

Pradeep
pradeep@nuview.com

-----From: bop@gandalf.se

Even more concerning is that "delete this" assumes that you are
allocating the object on the heap. What if the object is allocated
on the stack?

I also believe that "delete this" in the constructor does NOT
call virtual destructors for derived objects. This could introduce
further problems.


Bo Persson
bop@gandalf.se
-----From: Mike Blaszczak 


I guess this one is going to turn interesting: I had a few minutes to clean
up the code and try to reproduce the problem. Even after properly catching
the exception and deleting it, this code still leaks some memory.
It shouldn't, because VC++ 4.1 and VC++ 4.2 have compilers that are supposed
to properly unwind the allocations made in a try block.

The code doesn't seem to be getting set up--you can easily demonstrate that
it works if you use something that's _not_ derrived from CObject, so maybe
there's a problem in the compiler that MFC should work around. I'll have to
get this in front of the compiler guys to figure out for sure, though.

.B ekiM



Mike Bergman -- mbergman@datatree.com
Monday, August 19, 1996

[Mini-digest: 3 responses]

>
>Environment: VC+4.1, MFC4.1, Win 95, Win NT4 beta2
>
>SUMMARY
>One of the CFile constuctors throws an exception without cleaning up,  =20
>causing a memory leak.
>
>void CBugDlg::OnMsgbox()
>{
> UINT flags =3D CFile::modeRead | CFile::shareDenyNone;
> CString strFile =3D "NoFile.txt";  // This file does not exist
>=20
> try
> {
>  CFile* pFile =3D new CFile(strFile, flags); // Constructor throws an  =20
>exception
>       // Assignment to pFile never happens.
>  delete pFile;    // Never get here.
> }
> catch(CFileException e)
> {
>  TRACE("Caught an Exception")
> }
>}
>
>The constructor throws an exception and pFile is never assigned. On  =20
>quitting the program, a 16 byte CObject memory leak is detected.  =20
> _CrtSetBreakAlloc() confirms that this is the object created by the new  =
=20
>CFile.

The first problem you are having (why you are throwing the exception in the
first place) is that=20
you are trying to open a file that does not exist for reading
("NoFile.txt").  If the file does not exist then you should be using a
CFile::modeCreate.  If your function does not know whether a file exists
initially and _cares_ about it, then you should call findfirst() (or
whatever flavor is currently in vogue) then set the flag to modeCreate or
modeRead.  There are other ways around this, I won't go into them... you
should go back a study your CFile class for other options.

The second problem is that you're putting your delete pFile in the try
rather than the catch.  As soon as you get an exception in your try, it will
jump to the catch.  Really, you should not be putting the delete in the
catch either as it should go at the end of the function or the end of the
program.  I am assuming that you are using new so that the CFile object
survives the scope of the current function... so it should go wherever you
are cleaning up your program before termination.

//---------------------------------------------------
// =B5=DF -- Michael Bergman
//       Datatree Corporation
//       Voice: (619) 231-3300 x130
//       Fax:   (619) 231-3301
//       mbergman@datatree.com
//=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D
//  Fear? I don't know the meaning of the word fear!
//  Come to think of it, I don't know the meaning of=20
//  the word courage either!          --Groucho Marx
//---------------------------------------------------

-----From: "Giles R. Chamberlin" 


Thanks to all who've responded to my enquiry.  I've learnt a lot,   
specifically:

 - no matter how many times you check, the code you post is never the   
version you meant to.  Catching
CFileException rather than a pointer was just plain wrong!

 - delete this.  I was getting blinded by the trees.  Yup, it would be a   
disaster to try to free the memory twice
if the object was allocated on the stack.

 - and sometimes, just sometimes, the bugs may not be (totally) down to   
me.

So, wiser and almost confident of handling exceptions without someone to   
hold my hand I proceed.

Thanks,
Giles Chamberlin
Softwright  
-----From: Mike Blaszczak 


I did a little debugging with respect to this problem.  A lot of the responses
posted to the list for this question are just plain wrong, I'm afraid.
It's recently come to my attention that people think I'm a "Pr*ck" for
trying to teach people why what they think is wrong isntead of just coughing
up answers, so I'll try to stick to regurgitating facts here without any
reason or background.  All of this is spelled out if you look in the right
places, but explaining the details of the documentation also makes me a "Pr*ck".

So: the code the original poster provided was flawed--it would leak memory
no matter what because it never cleaned up the CException object that was
thrown.  (When you catch a CException-derived exception, you need to call
the Delete() member of that object to clean up the exception.)  Most people
who responded didn't point this out.

Even doing that, though, the code still leaks memory.  After applying the
fix, the code could look something like this:

void CMyDialog::OnOK()
{
   UINT uFlags = CFile::modeCreate;

   try {
      CFile* pFile = new CFile("This file ain't there.txt", uFlags);
      // party on the file
      pFile->Close();
      delete pFile;
   } catch (CException* pEX)
   {
      pEX->ReportError(); // tell the user
      pEX->Delete();
   }
}

this code would live in an MFC program. There's nothing wrong with this
code: it has zero defects and can be registered with ISO-9001 as such.

You can't use a special value (eg, NULL) to initialize pFile and notice
that it was ever allocated. Since the constructor is run during the call
to new up there, if the constructor  throws an exception, new never
returns and therefore never assigns its valuie to pFile. This approach
just assures that pFile is NULL all the time.

The ANSI UnStandard committtee finally spelled out what should happen in
this case about six months ago. They said that the compiler has to make
sure that the memory gets deleted before the exception is actually
thrown. Nobody in the world (except, maybe, the memory manager) has 
a copy of the pointer that new actually allocated. Only the compiler
(for a brief little heartbeat) and runtime library know what happend,
so they must be the ones to chunk out code to delete the problem.

Since the UnStandard folks finally specified this only recently, you
need to have VC++ 4.1 or VC++ 4.2 to get the cleanup code. The cleanup
code _might_ be in VC++ 4.0, but I really can't remember.  If you
have those versions, you can create a trivial program that shows this
mechanism really does, indeed, work. Try this, for example:

// compile me with
//      cl /MTd /GX thisfile.cpp

#ifndef _DEBUG
#error This program is usless without _DEBUG.  Use /MTd or /MDd.
#endif

#include 
#include 

class CExplosion
{
   char szSwollen[1234]; // make our size very big and identifiable
public:
   CExplosion() { throw 39; }
   ~CExplosion() { }
};


void tester()
{
   try
   {
      CExplosion* pEx = new CExplosion;
      delete pEx;    // maybe it didn't throw
   } 
   catch (int x)
   {
      printf("Got exception %d\n", x);
   }
}

void main()
{
   _CrtMemState s1, s2, s3;
   _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
   _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDOUT);
   _CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
   _CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDOUT);
   _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
   _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDOUT);

   _CrtMemCheckpoint(&s1);

   tester();

   _CrtMemCheckpoint(&s2);
   if (_CrtMemDifference(&s3, &s1, &s2))
      _CrtMemDumpStatistics(&s3);
   _CrtDumpMemoryLeaks();
}

You'll see that nothing is dumped by the CRTL's diagnostics.  If you
know assembler language, you can disassemble the code the compiler
makes for the tester() function and see that it's tossing around
some flags to make sure there is no problem.

The rub comes in when you add what most MFC modules have in
the first few lines of their code.  It looks something like this:

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

If you swipe the declaration of DEBUG_NEW that MFC has in AFX.H
(around line 1580 in MFC 4.2), which is just one more define that
looks like this:

#define DEBUG_NEW new(THIS_FILE, __LINE__)

and you'll also need the operator new override that's provided
in AFXMEM.H, too:

void* __cdecl operator new(size_t nSize, char* pszFileName, int nLine)
{
   return ::operator new(nSize, _NORMAL_BLOCK, pszFileName, nLine);
}

and add it _before_ everything we have above (but after,
of course, the #include directives), you'll be accurately simulating
the MFC compile-time environment. And, if you recompile the test
program above with those changes, you will see that the program
_DOES_ leak memory.

There is _no_ bug in the CFile constructor. MFC has every right
to expect that the compiler behaves within the standard, and it
doesn't, so we spring a leak.

Even given this discussion, though, there _could_ have been a bug
in the constructor that _would_ cause a leak.  As someone else
pointed out, only completely constructed objects are destructed.
That means, in the example of CFile, if the CFIle constructor throws
an exception, it isn't fully constructed and therefore the CFile
destructor will not run in that case. However, since CFile derives
from CObject, and the CObject constructor has run fully (because
objects are constructed from "bottom to top"), you know that the
CObject destructor will run if CFile throws an exception but
the CFile destructor _won't_ run.

So, you could make a bug:

class CExplosion
{
   char* pszBuffer;
public:
   CExplosion() {
      pszBuffer = new char[4096];
      if (some_condition)
         throw 125;
   } 
   ~CExplosion() {
      delete [] pszBuffer;
   }
};

CExplosion has a bad constructor becaue it will leak 4096 bytes each
time it is called and throws an exception. The memory allocated by
the constructor isn't freed because the destructor never runs if the
exception is thrown.  The proper way to code this is:

class CExplosion
{
   char* pszBuffer;
public:
   CExplosion() {
      pszBuffer = new char[4096];
      if (some_condition)
      {
         delete [] pszBuffer; // won't do it later
         throw 125;
      }
   } 
   ~CExplosion() {
      delete [] pszBuffer;
   }
};

Anyway, back to the issue at hand: with the DEBUG_NEW macro hooked
up, the code ends up calling an override of global operator new that 
knows how to track file names and line numbers for leaked memory.
In my example, the call

    CExplosion* pEx = new CExplosion;

ends up invoking those macros to eventually resolve to something like

    CExplosion* pEx = new("thisfile.cpp", 52) CExplosion;

which gives the new operator the information it needs to track the
problem. But that override causes the compiler to forget that it
needs to emit the cleanup code.

So, here are your choices:

1) Change your code to not use dynamically allocated CFile's.  CFile
 was written back when the compiler didn't understand exceptions
 _at all_, so there are ways to use the class that don't rely on
 any of these mechanisms.

2) Change your code so that it doesn't use DEBUG_NEW.  You'll still
 get leak detection, but the leaks won't be identified by line
 number and file-name. It's up to you to decide if that's more or
 less important than phony leaks.

It's worth noting that, when you compile for release mode, the
new-opreator defined-symbol switchorama isn't in effect so you
won't have this problem in non-_DEBUG builds. That is, there's
no reason to sweat this bug for shipping code.

The compiler is clearly at fault here: I'm working with someone on
the compiler team to make sure the problem gets fixed up. We might
find a work around that we can tuck into MFC 4.2a, but I don't
think it looks very promising right now. Anything can happen, though.

.B ekiM
http://www.nwlink.com/~mikeblas   <--- trip report central
1995 Honda VFR750F (Serial number 00050!)  4 Corners 1996!
1987 Yamaha FZ700 (damaged)                AMA, HRC, VFROC
     These words are my own: I do not speak for Microsoft.



Ash Williams -- ash@digitalworkshop.co.uk
Tuesday, August 27, 1996

Mike,
Personally I don't think anyone's a prick for giving helpful hints and 
advice.

As for CFile, my strategy is not to bother to call the buggy 
constructor, which doesn't clear up after itself, rather I use the Open 
function:

extern g_name;
extern g_flags;
void Yo
{
    CFileException e; // use this information if you want to

    CFile* pFile = new CFile; // ctor never throws ( but new might )
    if( pFile -> Open( g_name, g_flags, &e ) ) // Open never throws
    {
        // success!
    }
    else
    {
        pFile -> Abort(); // Abort never throws
        FailMessage();
    }
    
    delete pFile;
}

Ash




Pradeep Tapadiya -- pradeep@nuview.com
Thursday, August 29, 1996


Hi,

Just wanted to point it out that when "new" throws an exception,
pFile is not pointing to NULL. It is pointing to whatever
location it picked up on the stack. Therefore, delete pFile
may produce some unexpected results.

Please take this as a constructive criticism.

Pradeep
pradeep@nuview.com

>void Yo
>{
>    CFileException e; // use this information if you want to
>
>    CFile* pFile = new CFile; // ctor never throws ( but new might )
>    if( pFile -> Open( g_name, g_flags, &e ) ) // Open never throws
>    {
>        // success!
>    }
>    else
>    {
>        pFile -> Abort(); // Abort never throws
>        FailMessage();
>    }
>    
>    delete pFile;
>}
>
>Ash
>
>




Bradley V. Pohl -- brad.pohl@pobox.com
Saturday, August 31, 1996

>Just wanted to point it out that when "new" throws an exception,
>pFile is not pointing to NULL. It is pointing to whatever
>location it picked up on the stack. Therefore, delete pFile
>may produce some unexpected results.

>Please take this as a constructive criticism.

Some meta-level contructive criticism:  you probably should have looked at the code 
a bit more carefully before issuing your "contructive criticism."  

If operator new throws an exception, the "delete pfile" line will _not_ be executed because 
no try {} block is present to catch the thrown exception.  The call stack will continue
to unwind past the body of this function.

--Brad

>Pradeep
>pradeep@nuview.com

>void Yo
>{
>    CFileException e; // use this information if you want to
>
>    CFile* pFile = new CFile; // ctor never throws ( but new might )
>    if( pFile -> Open( g_name, g_flags, &e ) ) // Open never throws
>    {
>        // success!
>    }
>    else
>    {
>        pFile -> Abort(); // Abort never throws
>        FailMessage();
>    }
>    
>    delete pFile;
>}
>
>Ash



Pradeep Tapadiya -- pradeep@nuview.com
Monday, September 02, 1996

At 09:35 PM 8/31/96 -0400, you wrote:
>>Just wanted to point it out that when "new" throws an exception,
>>pFile is not pointing to NULL. It is pointing to whatever
>>location it picked up on the stack. Therefore, delete pFile
>>may produce some unexpected results.
>
>>Please take this as a constructive criticism.
>
>Some meta-level contructive criticism:  you probably should have looked at
the code 
>a bit more carefully before issuing your "contructive criticism."  
>
>If operator new throws an exception, the "delete pfile" line will _not_ be
executed because 
>no try {} block is present to catch the thrown exception.  The call stack
will continue
>to unwind past the body of this function.
>

I realized this just after I sent the e-mail out. My apologies. Guess I
made a fool of myself :(.

Pradeep





| Вернуться в корень Архива |