Elysia said…
When you allocate memory, an exception can be thrown. If that happens, you've already deleted your memory, so your string is in an "inconsistent state". Your memory is freed, yet the string class thinks it's not because string points to freed memory and no other members, such as len are updated. That's why
laserlight suggested you should allocate memory first, copy over the string, and only if that succeeded, then and only then, should you free the memory. That guarantees that you won't get this "inconsistent state."
Unless one uses std::nothrow and tests the return from new, new failures seem to simply crash the app. For example, this attempt to allocate 4 billion bytes on 32 bit boxes crashes at the call to new...
Code:
// This Crashes! ( x86 )
// cl MemTest1.cpp /O1 /Os /GS- /MT /EHsc
// 103,936 Bytes
#include <cstdio>
int main()
{
size_t iNumber;
char* pCh=NULL;
iNumber=4000000000;
pCh=new char[iNumber];
printf("pCh = %p\n",pCh);
if(pCh)
delete [] pCh;
else
printf("Memory Allocation Failed!\n");
getchar();
return 0;
}
At least that's what I'm seeing on Windows 10 when compiling with VC19 (VS 2015) x86. So therefore I don't see any justification for worrying about a string object being in an indeterminate state if the whole app has already crashed. All of which just rephrases the issue.
The only way I know of to prevent a crash if std::nothrow isn’t being used to handle the bad alloc exception is to use a try/catch exception handling block. Is there any other way? This works (64 bit code; Microsoft Compiler)…
Code:
// cl Exceptions.cpp /O1 /Os /MT /GS- /EHs
// 131,072 Bytes
#include <cstdio>
int main()
{
size_t iNumber=0;
wchar_t* pCh=NULL;
iNumber=400000000000000000;
try
{
pCh=new wchar_t[iNumber];
printf("pCh = %p\n",pCh);
}
catch(...)
{
printf("Memory Allocation Error!\n");
}
getchar();
return 0;
}
Having said that, I want to thank Laserlight and Elysia for their suggestions on this and having caused me to think about it. That code blurb I posted above is just about the way my String Class has been for like 10 years and has never caused me any trouble. But that’s simply because I very seldom need large strings for the work I do, or allocate so many of them that allocation failures are likely. An allocation failure with my code as I posted it would surely GPF. So I’ve spent the past two days updating my code to make it better, hopefully.
What I finally decided to do is a bit hard to describe, but here goes. I use both GCC and MS VC, but for MS VC I do some builds with the /nodefaultlib linker settings as I have my own versions of the C and C++ libraries parts of which I’ve written myself, and other parts obtained from reliable sources, and I link against them instead. However, sometimes I do use the standard linkages with MS VC. If I’m linking against my own library which I’ve named TCLib.lib (Tiny C Lib) I can simply use C++ new ‘as is’ because my implementations of operator new and operator new[] are just simple wrappers around Windows Api HeapAlloc(), which returns NULL on allocation failures, and as can be seen below, that NULL is simply returned to the point of call and can be tested…
Code:
//=====================================================================================
// Developed As An Addition To Matt Pietrek's LibCTiny.lib
//
// LIBCTINY -- Matt Pietrek 2001
// MSDN Magazine, January 2001
//
// With Help From Mike_V
//
// By Fred Harris, January 2016
//
// cl newdel.cpp /GS- /c /W3 /DWIN32_LEAN_AND_MEAN
//=====================================================================================
#include <windows.h>
void* __cdecl operator new(size_t s)
{
return HeapAlloc(GetProcessHeap(), 0, s);
}
void __cdecl operator delete(void* p)
{
HeapFree(GetProcessHeap(), 0, p);
}
void* operator new [] (size_t s)
{
return HeapAlloc(GetProcessHeap(), 0, s);
}
void operator delete [] (void* p)
{
HeapFree(GetProcessHeap(), 0, p);
}
So my revised operator= in my String Class now looks like this…
Code:
String& String::operator=(const String& strAnother)
{
if(this==&strAnother)
return *this;
if(strAnother.iLen > this->iCapacity)
{
size_t iNewSize=(strAnother.iLen*EXPANSION_FACTOR/MINIMUM_ALLOCATION+1)*MINIMUM_ALLOCATION;
TCHAR* pCh=NEW TCHAR[iNewSize];
if(pCh)
{
delete [] this->lpBuffer;
this->lpBuffer=pCh;
_tcscpy(this->lpBuffer,strAnother.lpBuffer);
this->iCapacity=iNewSize-1;
this->iLen=strAnother.iLen;
this->blnSucceeded=TRUE;
}
else
{
this->blnSucceeded=FALSE;
}
}
else
{
_tcscpy(this->lpBuffer,strAnother.lpBuffer);
this->iLen=strAnother.iLen;
this->blnSucceeded=TRUE;
}
return *this;
}
…where I simply test for a non-NULL return from new before doing the delete/copy operation. And that works when built against the standard libraries because I used my NEW macro instead of new for the call, and I defined my macro NEW like so…
Code:
#ifdef TCLib
…
#define NEW new
…
#else
…
#include <new>
#define NEW new(std::nothrow)
…
#endif
And I added another private data member variable to my String Class named blnSucceeded, which I can test (there’s an accessor too) if I feel an allocation error would be a possibility, in the same way a try/catch block would be used in a more standard C++ app. I don’t believe C++ Exception Handling would work with my TCLib.lib. Anyway, here’s a 64 bit test app using my String Class and TCLib.lib where I attempt to allocate 32 billion wchar_ts or 64 billion bytes for the text “Hello, World!”…
Code:
// cl Demo24.cpp Strings.cpp /O1 /Os /GS- TCLib.lib kernel32.lib user32.lib
// 5,120 Bytes VC19 (VS 2015) x64 UNICODE
// Test Machine HP Envy Laptop Windows 10 Home x64 16 Gigabytes RAM
#define UNICODE
#define _UNICODE
#include <windows.h> // 32,000,000,000, i.e., 32 billion wchar_ts -- 64,000,000,000 bytes succeeds
#include "stdlib.h" // 320,000,000,000, i.e., 320 billion wchar_ts -- 640,000,000,000 bytes fails
#include "stdio.h"
#include "tchar.h"
#include "Strings.h"
extern "C" int _fltused=1;
int main()
{
//size_t iNumber = 320000000000; // Fails
size_t iNumber = 32000000000; // Succeeds
String s1(iNumber,false); // The ‘false’ 2nd parameter just indicates the memory doedn’t have to be zeroed out
if(s1.blnSuccess())
{
s1=L"Hello, World!";
printf("Allocation Successful!\n");
s1.Print(L"s1.lpStr() = ",true);
printf("s1.Capacity() = %Iu\n",s1.Capacity());
printf("s1.Len() = %Iu\n",s1.Len());
}
else
{
printf("Allocation Failed!\n");
printf("s1.lpStr = %Iu\n",s1.lpStr());
}
getchar();
return 0;
}
That actually worked. Here’s the output…
Code:
Allocation Successful!
s1.lpStr() = Hello, World!
s1.Capacity() = 32000000015
s1.Len() = 13
But if I switch comments on my iNumber variable above and attempt to allocate 320 billion wchar_ts I get this (but no crash)…
Code:
Allocation Failed!
s1.lpStr = 0
The above code compiles to just 5 k with my TCLib!
So thanks again for the heads up Laserlight and Elysia. I’m happier now. I mostly do Win32 coding and use the memory allocation functions provided there instead of new from C++, and in that context I always test memory allocation failures. But somehow I left that bad usage in my String Class code. The only reason I can come up with for my dereliction in that regard is that when I first started writing this String Class code a long time ago I may have found out that allocation failures weren’t being indicated by a NULL return, but rather by an exception or crash, and perhaps I didn’t know what to do about it, and hoped it wouldn’t ever fail (and it never did). Its only been the past couple years that I learned about the std::nothrow option of new and started using it.
If you are still following this thread FanLi and are interested in my whole String Class, I just posted my only 2 day old updated version here in post #25…
Modeless MessageBox() Internals
Note To Laserlight…
About the magic numbers…
They are lapses as you certainly know. I’ve been ripping and tearing at my String Class for 10 years, at least. MINIMUM_ALLOCATION and EXPANSION_FACTOR have always been defined, but in the heat of doing battle its always easier to type 16 or 2 instead. So they seem to have multiplied or crept in some over the years. I fixed them all in my rewrite though!