The bad_alloc Constructor Parameter Proposal
By Richard Sposato
September 2017
Contents
A proposal to add two constructors to the std::bad_alloc class so it conforms to other exception classes.
Intended Audience:
These changes are intended for C++ programmers of intermediate through expert skill level who are either authors or users of memory allocation libraries.
The current implementation of bad_alloc class supports only the default constructor. To avoid breaking existing code, this constructor will continue to exist, and will provide an implementation defined explanatory string for the what() function.
bad_alloc();
The two additional constructors will accept either a reference to const std::string or a pointer to a C-style string. These constructors will look identical to constructors in other exception classes.
explicit bad_alloc( const std::string & what_arg );
explicit bad_alloc( const char * what_arg );
An alternate constructor would use a string_view parameter instead of either of the two above constructors.
explicit bad_alloc( std::string_view what_arg );
Each will construct a bad_alloc exception object that stores what_arg as an explanatory string that can be accessed through the what() function.
There are several motivations for this proposal. Each of these will be described in detail.
1. Conformance to existing exception classes.
2. Ease of making templates that construct exceptions.
3. Ease of deriving subclasses from exception classes.
4. Changes to C++17 new and delete operators.
5. Allowing allocators to inform callers exactly why allocations failed.
1. Conformance to existing exception classes.
All other exception classes in the std namespace provide constructors that accept either a single parameter of either a reference to a const std::string or a pointer to a C-style string. The parameter provides a human-readable explanation for why the program threw an exception or actionable information allowing the calling code to correct the problem.
· logic_error
· invalid_argument
· domain_error
· length_error
· out_of_range
· runtime_error
· range_error
· overflow_error
· underflow_error
· tx_exception
Conformance to other exception classes will enable motivations two and three for this proposal.
2. Ease of making templates that construct exceptions.
3. Ease of deriving subclasses from exception classes.
Motivations #2 and #3 are related. They can be explained with a single example that uses both templates and inheritance to show why bad_alloc constructors should accept an explanatory string parameter.
Classes that derive from the standard exceptions cannot call the bad_alloc constructor using the same code they would to call other exception classes. This example illustrates how to wrap existing exception classes with a smart-exception class that derives from standard exceptions.
template < class ExceptionType >
class SmartException
{
public:
SmartException( const char * message,
unsigned int line, const char * function ) :
ExceptionType( message ),
line_( line ),
functionName_( function )
{}
// ... rest of class ...
private:
unsigned int line_;
const char * functionName_;
};
The SmartException constructor will call a subclass constructor that accepts a string parameter. This works for most exceptions, but not bad_alloc.
These lines define various smart exceptions using the above class and typedefs.
typedef SmartException< std::bad_alloc > SmartBadAllocException;
typedef SmartException< std::logic_error > SmartLogicErrorException;
This function shows how code would throw a smart exception so any code catching the exception can report exactly where the problem occurred.
void * Allocator::Allocate( std::size_t bytes, std::size_t alignment )
{
void * place = FindPlace( bytes, alignment );
if ( place == nullptr )
{
throw SmartBadAllocException( “Error! Unable to allocate bytes.”,
__LINE__, __FUNCTION__ );
}
return place;
}
4. Changes to C++17 new and delete operators.
There are several additional new and delete operators in the C++17 standard. The additional operators all have a std::align_val_t parameter to support alignment-aware allocators. The std::align_val_t parameter will become part of class-specific new and delete operators along with the global operators.
void * operator new( std::size_t count, std::align_val_t al);
void * operator new[]( std::size_t count, std::align_val_t al);
void operator delete( void * ptr, std::size_t sz, std::align_val_t al );
void operator delete[]( void * ptr, std::size_t sz, std::align_val_t al );
Alignment-aware allocators would be unable to allocate the required number of bytes if the alignment is incorrect. (e.g. – The caller requests alignment on 16 byte boundaries, but the allocator only supports alignment on 4 or 8 byte boundaries.) If the allocator throws a bad_alloc exception for the invalid alignment, the caller may assume the program is out of memory instead of assuming the alignment is incorrect. Such a caller may call a new_handler believing it will make more memory available. A new_handler could spend an enormous amount of time looking for memory that is available only to find none, simply throw another bad_alloc exception, or terminate the program by calling std::abort or std::exit. None of these actions will resolve the simple problem of using the correct alignment.
A caller that is correctly informed the alignment is invalid could merely repeat the request with a different alignment value. This is a low cost action that provides the intended result – allocating a chunk of memory with the correct alignment.
This means that a bad_alloc exception with a valid explanatory string could provide the caller with actionable information to solve the problem, while a bad_alloc exception with no (or an inaccurate) explanatory string would lead the caller to perform an expensive or program-ending action.
When the std::align_val_t parameter is added to class-specific new and delete operators, users will be able to choose alignments that are not supported by the allocators they use. (This proposal assumes the global new and delete operators will support all alignment values in the std::align_val_t type, and thus it will be impossible to call a global memory operator with an invalid alignment value.)
5. Allowing allocators to inform callers exactly why allocations failed.
Allocations may fail for reasons other than the program ran out memory. There are specialized allocators that only handle requests for specific sizes, such as pool allocators, that could throw exceptions if they receive requests for sizes they cannot handle.
This example shows how an allocator might throw a bad_alloc exception with actionable information.
void * PoolAllocator::Allocate( std::size_t bytes, std::align_val_t alignment )
{
if ( ( bytes < minAllowedSize ) || ( maxAllowedSize < bytes ) )
{
throw SmartBadAllocException(
“Error! This allocator only handles allocations from 16 through 64 bytes in size.”, __LINE__, __FUNCTION__ );
}
if ( !IsValidAlignment( alignment ) )
{
throw SmartBadAllocException(
“Error! This allocator only handles alignments on 4 byte boundaries.”, __LINE__, __FUNCTION__ );
}
void * place = FindPlace( bytes, alignment );
if ( place == nullptr )
{
throw SmartBadAlloc( “Error! Unable to allocate bytes.”,
__LINE__, __FUNCTION__ );
}
return place;
}
· Implementing this proposal will have minimal impact on the C++ Standard.
· It is a pure extension that does not break any existing code.
· It does not require additional changes to any library or the existing language.
This proposal brings up various questions.
1. Should the bad_alloc exception be used only allocators fail from lack of memory?
2. Can it also be used for allocations that fail because of invalid parameters (e.g. wrong size or alignment)?
3. Can it also be used for allocations that fail because of internal problems with the allocator?
One answer to the first and second questions is that allocators should throw an out_of_range or invalid_argument exception when the size or alignment parameter is invalid. Likewise, one could say allocators should throw logic_error or runtime_error for internal problems. These answers imply bad_alloc is reserved strictly for out of memory conditions, but not for other conditions that arise during allocations.
However, many programmers write code assuming that allocators only throw bad_alloc. They do not expect to catch logic_error or invalid_argument exceptions, so these exceptions will propagate past the functions that should have caught them. For that reason, I recommend using bad_alloc for any problems that arise during allocations.
Many thanks to the following people who reviewed various drafts of this proposal and provided feedback.
1. Herb Sutter
2. Andrei Alexandrescu
1. http://en.cppreference.com/w/cpp/memory/new/bad_alloc
2. http://en.cppreference.com/w/cpp/error/logic_error
3. http://en.cppreference.com/w/cpp/memory/new/operator_new
4. http://en.cppreference.com/w/cpp/memory/new/operator_delete
5. http://en.cppreference.com/w/cpp/memory/new/set_new_handler
I have a technical question. The proposed signatures look like
bad_alloc needs to store a copy of
the data passed as an argument for the constructor. How likely is
storing that copy to fail right
after an allocation failure?
I have a follow-up question: did you consider the alternative where
the caller of the new constructor must pass an rvalue reference
to a std::string, in which case there would be most likely a much
smaller allocation, as in the case of a dynamically
allocated string that doesn't fit into SSO, there would be none, and
in the SSO case, a very small allocation that
is not dynamic?
Another question I have is this: what sort of ABI impact will this
change have for existing standard library implementations?
Hi,
I wrote this proposal after discovering the std::bad_alloc exception class only has a default constructor. Other exception classes have constructors with a string parameter. The lack of a similar constructor in bad_alloc has caused problems for me.
1. Should the bad_alloc exception be used only allocators fail from lack of memory?
2. Can it also be used for allocations that fail because of invalid parameters (e.g. wrong size or alignment)?
3. Can it also be used for allocations that fail because of internal problems with the allocator?
One answer to the first and second questions is that allocators should throw an out_of_range or invalid_argument exception when the size or alignment parameter is invalid. Likewise, one could say allocators should throw logic_error or runtime_error for internal problems. These answers imply bad_alloc is reserved strictly for out of memory conditions, but not for other conditions that arise during allocations.
However, many programmers write code assuming that allocators only throw bad_alloc. They do not expect to catch logic_error or invalid_argument exceptions, so these exceptions will propagate past the functions that should have caught them. For that reason, I recommend using bad_alloc for any problems that arise during allocations.
III. Motivation
There are several motivations for this proposal. Each of these will be described in detail.
1. Conformance to existing exception classes.
2. Ease of making templates that construct exceptions.
3. Ease of deriving subclasses from exception classes.
4. Changes to C++17 new and delete operators.
5. Allowing allocators to inform callers exactly why allocations failed.
1. Conformance to existing exception classes.
All other exception classes in the std namespace provide constructors that accept either a single parameter of either a reference to a const std::string or a pointer to a C-style string. The parameter provides a human-readable explanation for why the program threw an exception or actionable information allowing the calling code to correct the problem.
· logic_error
· invalid_argument
· domain_error
· length_error
· out_of_range
· runtime_error
· range_error
· overflow_error
· underflow_error
· tx_exception
Conformance to other exception classes will enable motivations two and three for this proposal.
2. Ease of making templates that construct exceptions.
3. Ease of deriving subclasses from exception classes.
Motivations #2 and #3 are related.
They can be explained with a single example that uses both templates and inheritance to show why bad_alloc constructors should accept an explanatory string parameter.
Classes that derive from the standard exceptions cannot call the bad_alloc constructor using the same code they would to call other exception classes. This example illustrates how to wrap existing exception classes with a smart-exception class that derives from standard exceptions.
template < class ExceptionType >
class SmartException
{
public:
SmartException( const char * message,
unsigned int line, const char * function ) :
ExceptionType( message ),
line_( line ),
functionName_( function )
{}
// ... rest of class ...
private:
unsigned int line_;
const char * functionName_;
};
The SmartException constructor will call a subclass constructor that accepts a string parameter. This works for most exceptions, but not bad_alloc.
template<class Exception>
class LocalizedException : Exception
{
template<typename ...Args>
LocalizedException(unsigned int line, const char * function, Args &&args)
: Exception(std::forward<Args>(args)...)
, line_(line)
, function_(function)
{}
};
4. Changes to C++17 new and delete operators.
There are several additional new and delete operators in the C++17 standard. The additional operators all have a std::align_val_t parameter to support alignment-aware allocators. The std::align_val_t parameter will become part of class-specific new and delete operators along with the global operators.
void * operator new( std::size_t count, std::align_val_t al);
void * operator new[]( std::size_t count, std::align_val_t al);
void operator delete( void * ptr, std::size_t sz, std::align_val_t al );
void operator delete[]( void * ptr, std::size_t sz, std::align_val_t al );
Alignment-aware allocators would be unable to allocate the required number of bytes if the alignment is incorrect. (e.g. – The caller requests alignment on 16 byte boundaries, but the allocator only supports alignment on 4 or 8 byte boundaries.) If the allocator throws a bad_alloc exception for the invalid alignment, the caller may assume the program is out of memory instead of assuming the alignment is incorrect. Such a caller may call a new_handler believing it will make more memory available. A new_handler could spend an enormous amount of time looking for memory that is available only to find none, simply throw another bad_alloc exception, or terminate the program by calling std::abort or std::exit. None of these actions will resolve the simple problem of using the correct alignment.
catch(std::bad_alloc &e)
{
if(e.what() == ...)
{
//Handle bad alignment
}
else
{
//Handle OOM.
}
}
5. Allowing allocators to inform callers exactly why allocations failed.
Allocations may fail for reasons other than the program ran out memory. There are specialized allocators that only handle requests for specific sizes, such as pool allocators, that could throw exceptions if they receive requests for sizes they cannot handle.
This example shows how an allocator might throw a bad_alloc exception with actionable information.
void * PoolAllocator::Allocate( std::size_t bytes, std::align_val_t alignment )
{
if ( ( bytes < minAllowedSize ) || ( maxAllowedSize < bytes ) )
{
throw SmartBadAllocException(
“Error! This allocator only handles allocations from 16 through 64 bytes in size.”, __LINE__, __FUNCTION__ );
}
if ( !IsValidAlignment( alignment ) )
{
throw SmartBadAllocException(
“Error! This allocator only handles alignments on 4 byte boundaries.”, __LINE__, __FUNCTION__ );
}
void * place = FindPlace( bytes, alignment );
if ( place == nullptr )
{
throw SmartBadAlloc( “Error! Unable to allocate bytes.”,
__LINE__, __FUNCTION__ );
}
return place;
}