【问题标题】:Memory allocation inside constructors?构造函数内部的内存分配?
【发布时间】:2016-07-29 07:39:23
【问题描述】:

我正在设计一个类似std::vector 的类用于自学目的,但我遇到了构造函数内部内存分配的难题。

std::vector 的容量构造函数非常方便,但代价是有可能抛出 std::bad_alloc 异常,并用它来破坏整个程序。

我正在努力决定什么是最优雅的方式来处理容量构造函数失败的不太可能的情况,或者最好地通知用户通过使用构造函数,他们同意数据结构能够删除整个程序通过异常。

我的第一个想法是在调用构造函数时添加一个编译时警告,提醒构造函数可能会失败,并且他们应该确保处理它,或者注意使用构造函数所涉及的风险。

这个解决方案看起来很糟糕,因为如果在全球范围内应用,会导致过多的警告,并给人留下不好的印象。

我的第二个想法是将构造函数设为私有,并要求通过静态“requestConstruct”-like 方法访问构造函数,类似于 Singleton 模式。

这个解决方案让界面看起来很奇怪。

我还有一些想法,但似乎都破坏了界面的“感觉”。这些想法是:

  • 类似升压的boost::optional
  • 类似 Haskell 的可能
  • 强制用户给我一个结构被授权的内存池。这个想法看起来很优雅,但我找不到 C/C++ 的干净/流行的静态内存池实现。不过,我确实在 https://github.com/CodeDmitry/MemoryPools 上成功进行了测试。
  • 使用描述性断言为炸毁世界道歉,并详细解释发生的事情。这就是我正在做的at the moment
  • 在调用它退出之前尝试再分配几次并使整个程序崩溃。这似乎是一个很好的策略,但感觉更像是交叉手指,因为程序仍然可能由于结构的行为(而不是程序)而崩溃。这种方法可能只是偏执和错误的,因为分配失败不一定会给您“重新分配”的机会,只会关闭程序。
  • 发明某种压力测试机制,让结构的所有者相信它可以处理用户期望的最大容量(这可能很难,因为这些压力测试可能会产生很大的误导,因为内存是可用的现在,但在内存更密集的时候,它可能不会)。

还有一个有趣的可能性是没有足够的内存来实际捕获异常。这个程序似乎没有捕捉到它(这可能与是否有足够的内存有关)。

#include <stdint.h>
#include <exception>
#include <iostream>
#include <stdlib.h>

int main(int argc, char **argv) 
{
    uint_fast32_t leaked_bytes = 0;

    while (1) {    
        try {
            new uint8_t;
        } catch (const std::bad_alloc& e) {
            std::cout << "successfully leaked" << leaked_bytes << " bytes." << '\n';
            exit(0);
        } catch (const std::exception& e) {
            std::cout << "I caught an exception, but not sure what it was...\n";
            exit(0);
        }   

        ++leaked_bytes;     
    }
}

这个程序确实让我在程序终止之前处理失败:

#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    uint_fast32_t bytes_leaked = 0;

    while (1) {
        if (malloc(1) == 0) {
            printf("leaked %u bytes.\n", bytes_leaked);
            exit(0);
        }        
        ++bytes_leaked;
    }

    return 0;
}

nothrow 也能正常工作:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <new>

int main(int argc, char **argv)
{
    uint64_t leaked_bytes = 0;

    while (1) {         
        uint8_t *byte = new (std::nothrow) uint8_t;
        if (byte == nullptr) {
            printf("leaked %llu bytes.\n", leaked_bytes);
            exit(0);
        }        
        ++leaked_bytes;
    }

    return 0;
}

编辑:

我想我找到了解决这个问题的方法。在主进程上存储动态数据结构存在固有的幼稚。也就是说,这些结构并不能保证一定会成功,并且可能随时中断。

最好在另一个进程中创建所有动态结构,并在出现意外问题时制定某种重启策略。

这确实增加了与数据结构通信的开销,但它使主程序不必管理数据结构可能出现的所有问题,从而可以迅速接管 main 中的大部分代码。

结束编辑。

是否有适当的方法来处理此类动态类中的分配问题,提高我的用户对这些风险的认识?

【问题讨论】:

  • 据我所知 std::vector 没有容量构造函数。它有 size 构造函数,它实际上分配对象而不仅仅是保留内存
  • 对我来说,抛出std::bad_alloc 是报告“内存不足”问题的最惯用方式。毕竟,如果无法获取内存,您的构造函数实际上无法完成其工作。
  • 做标准库做的事情。相信我,许多大胆的新人都试图做得更好。
  • 构造函数异常不会“取消整个程序”。他们可以很好地被抓住,你可以继续执行。如果内存不足,也许它应该“删除整个程序”,未被捕获。也许无效的输入会导致它请求不合理数量的内存,这会降低整个机器的速度,未经检查。
  • 我完全不明白这一点。您可以将构造函数放在trycatch 块中吗?否则,如果程序无法正常执行,是否要继续?

标签: c++ exception memory-management constructor malloc


【解决方案1】:

根据所提供的反馈,我确信没有优雅的方法可以防止管理动态内存的类在不引入复杂性的情况下占用您的程序。

抛出异常是有问题的,因为一旦你的类没有它可以分配的内存,它可能无法处理用户可以捕获的 std::bad_alloc。

通过更多的思考,我意识到防止程序从它使用的模块崩溃的一种方法是将使用这些模块的程序部分移动到另一个进程,并让该进程与主进程,因此如果其他进程以某种方式出现故障,您可以重新启动该进程并发出另一个请求。

只要您使用任何能够在极端情况下失败的类,就不可能防止它们失败,除非您能够控制它们的极端情况。

在动态内存的情况下,很难准确控制您的进程可以访问的动态内存、它具有的分配策略以及您的应用程序总共使用了多少内存。因此,如果不对您使用的连续内存池进行更严格的控制,或者将数据结构移动到另一个可以在失败时重新启动的进程管理,就很难控制它。

答案:本质上不可能同时提供管理动态内存的类的简单接口/实现对。您要么有一个简单的接口/实现,它将取消您的程序,例如 std::vector;或者一个复杂的接口/实现,需要以下之一或更聪明的东西:

  1. 程序员必须提供已经可用的内存块。由于内存已经存在并且您知道自己拥有多少内存,因此您不能分配超出您承受能力的内存。
  2. 数据结构使用自己的内存管理方案,使用会崩溃的单独进程而不是结构所在的进程。
  3. 数据结构完全由一个单独的进程管理,类似于数据库服务器。这样可以在失败时轻松重启流程。

【讨论】:

  • “抛出异常是有问题的”。如果您不信任您的语言及其标准库设计者,那么您可能选择了错误的语言。
  • 您可以在不分配任何内存的情况下抛出和捕获 bad_alloc。所以投掷没有问题。
  • 我会检查是否在检查一些编译器标志后再次尝试泄漏测试会让我捕捉到异常。现在它说“递归调用终止此应用程序已请求运行时以不寻常的方式终止它。请联系应用程序的支持团队以获取更多信息。.”编辑: g++ leak.cpp -o leak -O3 -s -DNDEBUG 没有改变任何东西。
  • 你的目标平台是什么?如果它是通用操作系统,则完全不用担心。即使捕获 bad_alloc 也是完全没用的。虽然它可能看起来过于激进,但确实如此。
猜你喜欢
  • 1970-01-01
  • 2017-08-17
  • 2015-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-22
  • 2021-08-09
  • 1970-01-01
相关资源
最近更新 更多