2

我即将在 C++ 中为 R 编写一个扩展包,并且想知道如何使用动态内存管理而不会有内存泄漏的风险。我读过了

并立即提出三个问题:

  1. 在出现 R 异常的情况下,R 是否会优雅地展开 C++ 堆栈帧,例如当R_alloc内存不足或Rf_error由于某些其他情况而被调用时?– 否则,我应该如何清理已经R_alloc'ed 和PROTECTed 或只是Calloc'ed 的内存?例如,将

    #include<R.h>
    // […]
    void someMethod () {
      char *buffer1 = NULL;
      char *buffer2 = NULL;
      try {
        ClassA a;
        buffer1 = R_Calloc( 10000, char );
        buffer2 = R_Calloc( 10000, char );
        // […]
      } finally {
        try {
          if ( NULL != buffer1 ) {
            R_Free( buffer1 );
          }
        } finally {
          if ( NULL != buffer2 ) {
            R_Free( buffer2 );
          }
        }
      }
    }
    

    保证调用析构函数~ClassAfor aand R_Freefor buffer1and buffer2? 如果不是,那么保证这一点的 R 教科书方法是什么?

  2. 可以使用标准 C++(现在已弃用)std::auto_ptr或现代C++std::unique_ptr来简化内存分配习惯吗?
  3. 是否有经过验证的 C++ 习惯用法/最佳实践在 C++ 标准模板库中使用 R 的内存分配,例如一些合适的分配器模板,以便 STL 类从 R 堆分配内存?
4

1 回答 1

0

由于 Rf_error 确实会跳过 C++ 堆栈帧并因此绕过析构函数调用,我发现有必要进行更多的文档研究。特别是查看 RODBC 包和实验监控内存使用以确认调查结果,让我得出:

1:立即将指针存储在 R 外部指针中并为此注册终结器。

下面这个有点简单的例子说明了这个成语:

#define STRICT_R_HEADERS    true

#include <string>
#include <R.h>
#include <Rinternals.h>     // defines SEXP

using namespace std;

class A {
    string name;
    public:
    A ( const char * const name ) : name( name ) { Rprintf( "Construct %s\n", name ); }
    ~A () { Rprintf( "Destruct %s\n", name.c_str() ); }
    const char* whoami () const { return name.c_str(); }
};

extern "C" {
    void finaliseAhandle ( SEXP handle ) {
        A* pointer = static_cast<A*>( R_ExternalPtrAddr( handle ) );
        if ( NULL != pointer ) {
            pointer->~A();
            R_Free( pointer );
            R_ClearExternalPtr( handle );
        }
    }

    SEXP createAhandle ( const SEXP name ) {
        A* pointer = R_Calloc( 1, A );
        SEXP handle = PROTECT( R_MakeExternalPtr(
            pointer,
            R_NilValue, // for this simple example no use of tag and prot
            R_NilValue
        ) );
        try {
            new(pointer) A( CHAR( STRING_ELT( name, 0 ) ) );
            R_RegisterCFinalizerEx( handle, finaliseAhandle, TRUE );
        } catch (...) {
            R_Free( pointer );
            R_ClearExternalPtr( handle );
            Rf_error( "construction of A(\"%s\") failed", CHAR( STRING_ELT( name, 0 ) ) );
        }
        // … more code may follow here, including calls to Rf_error.
        UNPROTECT(1);
        return handle;
    }

    SEXP nameAhandle ( const SEXP handle ) {
        A* pointer = static_cast<A*>( R_ExternalPtrAddr( handle ) );
        if( NULL != pointer ) {
            return mkChar( pointer->whoami() );
        }
        return R_NilValue;
    }

    SEXP destroyAhandle ( const SEXP handle ) {
        if( NULL != R_ExternalPtrAddr( handle ) ) {
            finaliseAhandle( handle );
        }
        return R_NilValue;
    }
}

NULL对 in 指针的赋值R_ClearExternalPtr( handle );防止了 R_Free(pointer);` 的双重调用。

请注意,建议的习惯用法仍然需要一些假设才能安全工作:如果构造函数不能在 R 的意义上失败,即通过调用Rf_error. 如果这无法避免,我的建议是将构造函数调用推迟到终结器注册之后,以便终结器在任何情况下都能够R_Free记忆。但是,必须包含逻辑才能不调用析构函数~A,除非A对象已被有效构造。在简单的情况下,例如当A只包含原始字段时,这可能不是问题,但在更复杂的情况下,我建议包装A到一个struct可以记住是否A构造函数成功完成,然后为该结构分配内存。当然,我们仍然必须依靠A构造函数来优雅地失败,释放它分配的所有内存,无论这是由C_allocormalloc还是其他人完成的。(实验表明,R_alloc在 的情况下,内存会自动释放Rf_error。)

2:没有。

这两个类都与注册 R 外部指针终结器无关。

3:是的。

据我所知,将 C++ 和 R 的统治完全分开被认为是最佳实践。Rcpp 鼓励使用包装器(https://stat.ethz.ch/pipermail/r-devel/2010-May/057387 .htmlcxxfunctionhttp://dirk.eddelbuettel.com/code/rcpp.html),这样 C++ 异常就不会命中 R 引擎。

在我看来,分配器可以被编程为使用R_CallocR_Free. 但是,为了在此类调用期间抵消潜在的影响Rf_error,分配器将需要一些垃圾收集接口。我想在本地将分配器绑定到一个PROTECTedSEXP类型externalptr,该类型具有一个由注册的终结R_RegisterCFinalizerEx器并指向一个本地内存管理器,该管理器可以在Rf_error.

于 2015-07-06T11:11:44.637 回答