Как провалить конструктор с новым (std :: nothrow)?

Рассмотрим следующий код:

#include <new>
#include <malloc.h>
#include <stdio.h>

void * operator new(size_t size) {
void *res;
if (size == 1) {
res = NULL;
} else {
res = malloc(size);
}
fprintf(stderr, "%s(%zu) = %p\n", __PRETTY_FUNCTION__, size, res);
if (res == NULL) throw std::bad_alloc();
return res;
}

void * operator new(size_t size, const std::nothrow_t&) {
void *res;
if (size == 1) {
res = NULL;
} else {
res = malloc(size);
}
fprintf(stderr, "%s(%zu) = %p\n", __PRETTY_FUNCTION__, size, res);
return res;
}

void operator delete(void *ptr) {
fprintf(stderr, "%s(%p)\n", __PRETTY_FUNCTION__, ptr);
free(ptr);
}

void operator delete(void *ptr, const std::nothrow_t&) {
fprintf(stderr, "%s(%p)\n", __PRETTY_FUNCTION__, ptr);
free(ptr);
}

class Foo { };

class Bar {
public:
Bar() : ptr(new Foo()) {
fprintf(stderr, "%s: ptr = %p\n", __PRETTY_FUNCTION__, ptr);
}
Bar(const std::nothrow_t&) noexcept : ptr(new(std::nothrow) Foo()) {
fprintf(stderr, "%s: ptr = %p\n", __PRETTY_FUNCTION__, ptr);
}
~Bar() noexcept {
delete ptr;
}
Foo *ptr;
};

class Baz {
public:
Baz() : ptr(new Foo()) {
fprintf(stderr, "%s: ptr = %p\n", __PRETTY_FUNCTION__, ptr);
}
~Baz() {
delete ptr;
}
Foo *ptr;
};

int main() {
Bar *bar = new(std::nothrow) Bar(std::nothrow_t());
if (bar != NULL) {
delete bar;
} else { fprintf(stderr, "bad alloc on Bar(std::nothrow_t())\n"); }
fprintf(stderr, "\n");
try {
bar = new(std::nothrow) Bar();
delete bar;
} catch (std::bad_alloc) { fprintf(stderr, "bad alloc on Bar()\n"); }
fprintf(stderr, "\n");
try {
Baz *baz = new Baz();
delete baz;
} catch (std::bad_alloc) { fprintf(stderr, "bad alloc on Baz()\n"); }
}

Это дает следующий вывод:

void* operator new(size_t, const std::nothrow_t&)(8) = 0x1fed010
void* operator new(size_t, const std::nothrow_t&)(1) = (nil)
Bar::Bar(const std::nothrow_t&): ptr = (nil)
void operator delete(void*)((nil))
void operator delete(void*)(0x1fed010)

void* operator new(size_t, const std::nothrow_t&)(8) = 0x1fed010
void* operator new(std::size_t)(1) = (nil)
void operator delete(void*, const std::nothrow_t&)(0x1fed010)
bad alloc on Bar()

void* operator new(std::size_t)(8) = 0x1fed010
void* operator new(std::size_t)(1) = (nil)
void operator delete(void*)(0x1fed010)
bad alloc on Baz()

Как вы можете видеть, выделение первого бара происходит успешно, несмотря на то, что распределение Foo не выполняется. Второе выделение Bar и распределение Baz завершаются неудачно с помощью std :: bad_alloc.

Теперь мой вопрос: как сделать «новый (std :: nothrow) Bar (std :: nothrow_t ());» освободить память для Bar и вернуть NULL, если Foo не удается выделить? Является ли инверсия зависимостей единственным решением?

3

Решение

Предположим, вы хотите иметь возможность иметь неудачную конструкцию без исключений, как правило.

Я нарисую такую ​​систему.

template<class Sig>
struct has_creator;
template<class T, class...Args>
struct has_creator<T(Args...)>

это класс черт, который происходит от true_type если твой тип T имеет статический метод, который соответствует подписи bool T::emplace_create(T*, Args&&...),

emplace_create возвращает false при ошибке создания. T* должен указывать на неинициализированный кусок памяти с надлежащим выравниванием и sizeof(T) или больше

Теперь мы можем написать это:

template<class T, class...Args>
T* create( Args&&... args )

которая является функцией, которая обнаруживает, если T has_creatorи, если это так, выделяет память, делает emplace_createи, если это не удается, он очищает память и возвращает nullptr, Естественно, он использует nothrow new,

Вы сейчас используете create<T> на месте new везде.

Большим недостатком является то, что мы не очень хорошо поддерживаем наследование. И композиция становится сложнее: мы в основном пишем наш конструктор в emplace_create и наш фактический конструктор почти ничего не делает, и в emplace_create мы обрабатываем случаи сбоя create<X> вызов).

Мы также не получаем никакой помощи с наследованием. Если нам нужна помощь с наследованием, мы можем написать два разных метода — один для первоначального построения без сбоев, а второй для создания ресурсов, подверженных сбоям.

Отмечу, что это становится менее раздражающим, если вы перестанете хранить сырые указатели в любом месте. Если вы храните вещи в std::unique_ptr везде (даже до такой степени, что create<T> возврате std::unique_ptr<T>), и добавьте в конец защищенного разрушителя конца области видимости, и ваш деструктор должен быть в состоянии обрабатывать «наполовину построенные» объекты.

0

Другие решения

C ++ 11 §5.3.4 / 18:

» Если какая-либо часть инициализации объекта, описанного выше, завершается с помощью исключения и подходящего
можно найти функцию освобождения, функция освобождения вызывается для освобождения памяти, в которой находится объект
был построен, после чего исключение продолжает распространяться в контексте новое выражение.

Так std::nothrow не гарантирует никаких исключений из новое выражение. Это просто аргумент, передаваемый в функцию выделения, которая выбирает функцию без выбрасывания из стандартной библиотеки. По-видимому, это в основном поддерживает больше предстандартного кода в стиле C.

Весь механизм очистки в современном C ++ основан на исключениях.

Чтобы обойти это — что я считаю глупым, не то, что нужно делать, но вы просите — например, сделать.

#include <iostream>
#include <new>
#include <stdexcept>
#include <stdlib.h>         // EXIT_FAILURE
#include <typeinfo>
#include <utility>

namespace my { class Foo; }

template< class Type, class... Args >
auto null_or_new( Args&&... args )
-> Type*
{
#ifdef NULLIT
if( typeid( Type ) == typeid( my::Foo ) ) { return nullptr; }
#endif

try
{
return new( std::nothrow ) Type( std::forward<Args>( args )... );
}
catch( ... )
{
return nullptr;
}
}

namespace my
{
using namespace std;

class Foo {};

class Bah
{
private:
Foo*    p_;

public:
Bah()
: p_( null_or_new<Foo>() )
{
clog << "Bah::<init>() reports: p_ = " << p_ << endl;
if( !p_ ) { throw std::runtime_error( "Bah::<init>()" ); }
}
};
}  // namespace my

auto main() -> int
{
using namespace std;
try
{
auto p = null_or_new<my::Bah>();
cout << p << endl;
return EXIT_SUCCESS;
}
catch( exception const& x )
{
cerr << "!" << x.what() << endl;
}
return EXIT_FAILURE;
}

Почему запрашиваемый подход ИМХО глуп:

  • Отказ от безопасности исключений. Нет гарантированной очистки при распространении сбоев. На самом деле нет гарантированного распространения сбоев, все это очень вручную.

  • Он отбрасывает всю информацию о сбое, например, сообщение об исключении. Можно добавить механизмы, чтобы сохранить некоторые из них, но это становится сложным и неэффективным.

  • У него нет правдоподобного преимущества, о котором я могу думать.


Попутно обратите внимание, что спецификатор формата %zu и макрос __PRETTY_FUNCTION__ не работает с Visual C ++.

Также обратите внимание, что для возврата нулевого указателя функция распределения должна быть объявлена ​​как невыбрасывающая.


добавление

Пример выполнения очень очень ручного действия, избегая даже внутренних исключений. Главным образом стоимость заключается в том, что человек отказывается от обычного механизма C ++, где только те элементы данных, которые уже успешно созданы, уничтожаются при обнаружении сбоя. Вместо этого все должно быть построено в фиктивных состояниях, чтобы объекты зомби временно доступно

#include <iostream>
#include <new>
#include <stdexcept>
#include <stdlib.h>         // EXIT_FAILURE
#include <typeinfo>
#include <utility>

namespace my { class Foo; }

struct Result_code { enum Enum { success, failure }; };

template< class Type, class... Args >
auto null_or_new( Args&&... args )
-> Type*
{
#ifdef NULLIT
if( typeid( Type ) == typeid( my::Foo ) ) { return nullptr; }
#endif

auto code = Result_code::Enum();
auto const p = new( std::nothrow ) Type( code, std::forward<Args>( args )... );
if( p != nullptr && code != Result_code::success )
{
p->Type::~Type();
::operator delete( p, std::nothrow );
return nullptr;
}
return p;
}

namespace my
{
using namespace std;

class Foo { public: Foo( Result_code::Enum& ) {} };

class Bah
{
private:
Foo*    p_;

public:
Bah( Result_code::Enum& code )
: p_( null_or_new<Foo>() )
{
clog << "Bah::<init>() reports: p_ = " << p_ << endl;
if( !p_ ) { code = Result_code::failure; }
}
};
}  // namespace my

auto main() -> int
{
using namespace std;
try
{
auto p = null_or_new<my::Bah>();
cout << p << endl;
return EXIT_SUCCESS;
}
catch( exception const& x )
{
cerr << "!" << x.what() << endl;
}
return EXIT_FAILURE;
}
2

По вопросам рекламы [email protected]