2014-11-17 3 views
3

Рассмотрим следующий код:Как свернуть конструктор с новым (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 и alloaction Baz должным образом происходит с использованием std :: bad_alloc.

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

+0

Должен ли быть синтаксис 'new (std :: nothrow) Bar (std :: nothrow_t());'? Как насчет 'Bar :: create (std :: nothrow_t {})'? Или 'create (std :: nothrow_t {})'? – Yakk

+0

Вы имеете в виду класс Bar {public: static Bar * create(); private: Bar (Foo * foo); }? Это будет использовать инверсию зависимостей. –

+0

Нет, 'Bar' все еще может создать' Foo'. Вы просто обнаруживаете отказ в своей функции-создателя и освобождаете и возвращаете нуль. Детали менее важны, чем тот факт, что мы больше не делаем это с 'new', и это может быть неприемлемо для вас. – Yakk

ответ

0

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

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

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

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

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

Теперь мы можем написать следующее:

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

, которая является функцией, которая определяет, если Thas_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

Это звучит как более причудливый способ использования заводских функций. Мне нужно будет увидеть, как это выглядит с полным кодом по сравнению с простыми заводскими функциями. FYI: Заметка об уникальном_ptr - золото. Я вижу, что я недостаточно изучил новые возможности C++ 11. Благодарю. –

+0

@GoswinvonBrederlow честно, это, вероятно, выглядит очень уродливо: это просто я spitballing решение в попытке покончить с исключениями, но дать исключение, как механика. – Yakk

2

C++ 11 §5.3.4/18:

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

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

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

Чтобы обойти это –, который, я думаю, глупо, не вещь, но вам ’ re ask – do eg.

#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; 
} 

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

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

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

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


Попутно заметим, что спецификатор формата %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; 
} 
+0

Код предназначен для работы без исключений, поскольку он должен использоваться в контекстах, где исключения недоступны. Но ваш код вызывает std :: runtime_error. Он также может выбросить std :: bad_alloc и сделать это «right [tm]». Цель состоит в том, чтобы вернуть указатель NULL, когда Bar не может быть полностью создан. –

+0

@ GoswinvonBrederlow В этом случае вы в значительной степени обречены. Конструктор ничего не может вернуть на C++, поэтому вы должны либо использовать фабричный метод той или иной формы, либо переместить код конструкции в отдельную функцию 'init()'. Это действительно один из недостатков дизайна на C++, если вы спросите меня. – cmaster

+0

@GoswinvonBrederlow: Без бросания у вас нет хорошего способа отступить от конструктора. Распределение всех необходимых ресурсов до создания и передачи этих ресурсов становится довольно уродливым. Но вы можете передать логическое или перечисляемое по ссылке, чтобы конструктор мог указать отказ на восстановление до заводской функции. –

Смежные вопросы