【问题标题】:How to fail a constructor with new(std::nothrow)?如何使用 new(std::nothrow) 使构造函数失败?
【发布时间】:2015-01-14 18:21:14
【问题描述】:

考虑以下代码:

#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 成功。 Bar 的第二次分配和 Baz 的分配通过使用 std::bad_alloc 正确失败。

现在我的问题是:如何制作“new(std::nothrow) Bar(std::nothrow_t());”当 Foo 分配失败时释放 Bar 的内存并返回 NULL?依赖倒置是唯一的解决方案吗?

【问题讨论】:

  • 语法必须是new(std::nothrow) Bar(std::nothrow_t());吗? Bar::create( std::nothrow_t{} ) 怎么样?还是create&lt;Bar&gt;( std::nothrow_t{} )
  • 你的意思是class Bar { public: static Bar * create();私人:酒吧(Foo *foo); }?那将使用依赖倒置。
  • 不,Bar 仍然可以创建Foo。您只需在创建者函数中检测到失败并取消分配并返回 null。细节不如我们不再使用 new 的事实重要,这对您来说可能无法接受。
  • 好的。与完全依赖倒置略有不同。但是为了以一般方式工作,类需要一个 is_good() 方法,必须检查构造函数和析构函数是否必须处理初始化错误的类。我希望仍然可以从您的自动清理中受益,但有例外。

标签: c++ c++11 constructor new-operator nothrow


【解决方案1】:

C++11 §5.3.4/18:

如果上述对象初始化的任何部分通过抛出异常和合适的 可以发现deallocation函数,调用deallocation函数释放对象所在的内存 正在构造,之后异常继续在 new-expression 的上下文中传播。

所以std::nothrow 不保证 new-expression 不会出现异常。它只是传递给分配函数的参数,从标准库中选择不抛出的参数。它显然主要是为了支持更多的 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;
}

【讨论】:

  • 代码可以在没有异常的情况下工作,因为它必须在异常不可用的上下文中可用。但是您的代码会引发 std::runtime_error。它还不如抛出 std::bad_alloc 然后“正确[tm]”执行它。目标是在 Bar 无法完全创建时返回一个 NULL 指针。
  • @GoswinvonBrederlow 在这种情况下,你几乎注定要失败。构造函数不能返回 C++ 中的任何内容,因此您必须使用一种或另一种形式的工厂方法,或者必须将构造代码移动到单独的 init() 函数中。如果你问我,这确实是 C++ 的设计缺陷之一。
  • @GoswinvonBrederlow:如果不抛出,您就没有退出构造函数的好方法。在构造之前分配所有需要的资源并传递这些资源,变得相当丑陋。但是您可以通过引用传递布尔值或枚举,以便构造函数可以将失败指示回工厂函数。
【解决方案2】:

假设您希望能够无例外地构建失败作为一般规则。

我将画出这样一个系统。

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&amp;&amp;...) 匹配的静态方法。

emplace_create 在创建失败时返回 false。 T* 必须指向一个未初始化的内存块,具有正确的对齐方式,sizeof(T) 或更大。

我们现在可以这样写:

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

这是一个检测Thas_creator是否分配内存的函数,如果是,则执行emplace_create,如果失败则清理内存并返回nullptr。自然它使用 nothrow new

您现在在任何地方都使用create&lt;T&gt; 代替new

最大的缺点是我们不能很好地支持继承。组合变得棘手:我们基本上在 emplace_create 中编写构造函数,让我们的实际构造函数几乎什么都不做,而在 emplace_create 中,我们处理失败的情况(例如子对象的 create&lt;X&gt; 调用失败)。

我们在继承方面也几乎没有任何帮助。如果我们需要继承方面的帮助,我们可以编写两种不同的方法——一种用于无失败的初始构造,第二种用于容易失败的资源创建。

我会注意到,如果您停止在任何地方存储原始指针,它会变得不那么烦人。如果你将东西存储在std::unique_ptr 的任何地方(甚至到让create&lt;T&gt; 返回std::unique_ptr&lt;T&gt; 的程度),并抛出一个带中止的受保护的作用域结束销毁器,你的析构函数必须能够处理“一半-constructed”对象。

【讨论】:

  • 这听起来像是一种更奇特的使用工厂函数的方式。与简单的工厂函数相比,我将不得不看看完整代码的外观。仅供参考:关于 unique_ptr 的注释是黄金。我发现我对新的 C++11 功能的研究还不够。谢谢。
  • @GoswinvonBrederlow 老实说,它可能看起来真的很丑:这只是我吐出一个解决方案,试图消除异常,但提供类似异常的机制。
猜你喜欢
  • 1970-01-01
  • 2014-04-26
  • 1970-01-01
  • 2013-02-23
  • 1970-01-01
  • 2017-04-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多