C++ new と null とコンストラクタ

まとめ

プロジェクトで例外を使うにしても使わないにしても例外構文が有効な状態でビルドする場合は operator new には必ず例外の有無を指定すること

malloc

組み込み系のアプリケーションでは処理系のヒープ関連関数を使うということはありません
普通は自前で実装したものを使います

この他にもメモリ境界を指定したりカテゴリーを指定したり呼び出し場所を指定したりするなどなどのバリエーションもあるでしょうが、簡単のためにここでは下記の二つがあるものとします

void* my_malloc( size_t cb );
void* my_free( void* p );

new

そして C++ であればそれを利用するために operator new と operator delete をオーバーロードします

void* operator new( size_t cb ){
	return my_malloc( cb );
}
void operator delete( void* p ){
	my_free( p );
}
void* operator new[]( size_t cb ){
	return my_malloc( cb );
}
void operator delete[]( void* p ){
	my_free( p );
}

CFoo* p = new CFoo( 引数 );

null

ここで、メモリが足りなくなると何が起きるでしょうか

自前のメモリ確保関数が NULL を返したときのために使っている側で適切なチェックを入れてあれば問題は起きませんが、そうでなければアクセス違反の例外が発生します

char* p = static_cast<char*>(my_malloc( バイト数 ));
// もし NULL が返ってきたら死ぬ!
.
.
.
my_free( p );

char* p = static_cast<char*>(my_malloc( バイト数 ));
if( !p ){
	// NULL が返ってきても適切なチェックが入っていれば安心
}
else{
	.
	.
	.
	my_free( p );
}

すると operator new も NULL を返しますから呼び出し側は同様です

CFoo* p = new CFoo( 引数 );
// もし NULL が返ってきたら死ぬ!
.
.
.
delete p;

CFoo* p = new CFoo( 引数 );
if( !p ){
	// NULL が返ってきても適切なチェックが入っていれば安心
}
else{
	.
	.
	.
	delete p;
}

本当にそうでしょうか

コンストラク

この式は operator new を呼び出した後に CFoo のコンストラクタを呼び出します

CFoo* p = new CFoo( 引数 );

ここで operator new が NULL を返すと何が起きるでしょうか
標準では次のように言っています

5.3 Unary expressions
5.3.4 New
13 [ Note: unless an allocation function is declared with an empty exception-specification (15.4), throw(), it indicates failure to allocate storage by throwing a bad_alloc exception (clause 15, 18.4.2.1); it returns a non-null pointer otherwise.
If the allocation function is declared with an empty exception-specification, throw(), it returns null to indicate failure to allocate storage and a non-null pointer otherwise.
- end note ]
If the allocation function returns null, initialization shall not be done, the deallocation function shall not be called, and the value of the new-expression shall be null.

メモリ確保関数に例外なし指定 throw() が指定されているときメモリ確保関数が NULL を返したら、初期化はされるべきでなく、new 式は NULL と評価されるべき。とあるのでコンストラクタは呼ばれないのが正しいようです

ただし「例外なし指定 throw() が指定されているとき」です
上記の宣言を見ると次のようになっていますね

void* operator new( size_t cb );
void operator delete( void* p );
void* operator new[]( size_t cb );
void operator delete[]( void* p );

この場合、例外指定がありませんからコンパイラはメモリが足りない時は std::bad_alloc 例外が発生するという前提、つまり NULL が返ってこない前提でコードを生成します
しかし実装は my_malloc を呼んでいるだけなのでメモリが足りないときは NULL を返します
すると何が起きるでしょうか

例外指定に拘らずいつでも NULL チェックをするコンパイラでは何も起きずただ NULL を返します
しかし NULL が返ってこない前提で NULL チェックを省略するようなコンパイラでは NULL に対してコンストラクタ呼び出そうとして死にます
どちらも標準に準拠した正しい挙動ですが死なれると困りますね
どうすればいいでしょうか

手動でやってもいいのですが面倒ですね

CFoo* p = static_cast<CFoo*>(my_malloc( sizeof( CFoo )));
if( !p ){
}
else{
	new(p) CFoo( 引数 );
	.
	.
	.
	p->~CFoo();
	my_free( p );
}

標準は「例外なし指定 throw() が指定されているときは NULL を返していい」と言っているのですからそれを付ければいいのです

void* operator new( size_t cb ) throw() {
	return my_malloc( cb );
}
void operator delete( void* p ) throw() {
	my_free( p );
}
void* operator new[]( size_t cb ) throw() {
	return my_malloc( cb );
}
void operator delete[]( void* p ) throw() {
	my_free( p );
}

operator new がこのように宣言されていて初めて呼び出し側で安全に NULL チェックができるようになります

CFoo* p = new CFoo( 引数 );
if( !p ){
	// NULL が返ってきても適切なチェックが入っていれば安心
}
else{
	.
	.
	.
	delete p;
}

余談

標準に次のようにあるのでコンストラクタの引数には副作用のあるものは渡してはいけません

n1905
21 Whether the allocation function is called before evaluating the constructor arguments or after evaluating the constructor arguments but before entering the constructor is unspecified.
It is also unspecified whether the arguments to a constructor are evaluated if the allocation function returns the null pointer or exits using an exception.

n3000
16 The invocation of the allocation function is indeterminately sequenced with respect to the evaluations of expressions in the new-initializer.
Initialization of the allocated object is sequenced before the value computation of the new-expression.
It is unspecified whether are evaluated if the allocation function returns the null pointer or exits using an exception.

×ダメ

CFoo* p = new CFoo( ++a );

nothrow

プロジェクトで例外を使いたくない場合は上記のようにすることで解決するのですが、基本的には例外を使い、何らかの事情で一部だけ例外を使わずに NULL チェックをしたいという場合もあるかもしれません
このような場合に対応するために標準では std::nothrow というものがあります

次のように new に std::nothrow を指定して呼び出すと明示的に例外を使わない new を呼び出すことができます

CFoo* p = new(std::nothrow) CFoo( 引数 ); 
if( !p ){
	// NULL が返ってきても適切なチェックが入っていれば安心
}
else{
	.
	.
	.
	operator delete( p, std::nothrow );
}

これは標準ライブラリのヘッダで次のようなオーバーロードが宣言されているからです

namespace std {
struct nothrow_t {};
extern const nothrow_t	nothrow;
}

void*	operator new( size_t	cb, std::nothrow_t ) throw();
void	operator delete( void*	p, std::nothrow_t ) throw();

ここで重要なのは throw() を指定された operator new を呼び出せるという機能ですから std::nothrow は別に自前の my_nothrow とかでも何でも構いません

struct my_nothrow_t {};
extern const my_nothrow_t	my_nothrow;
void*	operator new( size_t	cb, my_nothrow_t ) throw();
void	operator delete( void*	p, my_nothrow_t ) throw();

CFoo* p = new(my_nothrow) CFoo(...);

std::nothrow_t という型や std::nothrow というオブジェクトが特別な何かというわけではありません
ただ特定の目的のためにオーバーロードされた関数を呼び出すのに分りやすい名前が付いているというだけです