使用Rcpp连接字符串向量

4
3个回答

8

可能有几种不同的方法来解决这个问题,但这里提供一种使用 std::transform 的选项:

#include <Rcpp.h>
using namespace Rcpp;

struct Functor {
    std::string
    operator()(const std::string& lhs, const internal::string_proxy<STRSXP>& rhs) const
    {
        return lhs + rhs;
    }
};

// [[Rcpp::export]]
CharacterVector paste2(CharacterVector lhs, CharacterVector rhs)
{
    std::vector<std::string> res(lhs.begin(), lhs.end());
    std::transform(
        res.begin(), res.end(),
        rhs.begin(), res.begin(),
        Functor()
    );
    return wrap(res);
}

/*** R

lhs <- letters[1:2]; rhs <- letters[3:4]

paste(lhs, rhs, sep = "")
# [1] "ac" "bd"

paste2(lhs, rhs)
# [1] "ac" "bd"

*/ 

首先将左手表达式复制到一个std::vector<std::string>中的原因是,internal::string_proxy<>类提供了带有如下签名的operator+

std::string operator+(const std::string& x, const internal::string_proxy<STRSXP>& y) 

相比于,例如。
operator+(const internal::string_proxy<STRSXP>& x, const internal::string_proxy<STRSXP>& y) 

如果您的编译器支持C++11,那么可以更加简洁地实现此操作:
// [[Rcpp::plugins(cpp11)]]
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
CharacterVector paste3(CharacterVector lhs, CharacterVector rhs)
{
    using proxy_t = internal::string_proxy<STRSXP>;

    std::vector<std::string> res(lhs.begin(), lhs.end());
    std::transform(res.begin(), res.end(), rhs.begin(), res.begin(),
        [&](const std::string& x, const proxy_t& y) {
            return x + y;
        }
    );

    return wrap(res);
}

/*** R

lhs <- letters[1:2]; rhs <- letters[3:4]

paste(lhs, rhs, sep = "")
# [1] "ac" "bd"

paste3(lhs, rhs)
# [1] "ac" "bd"

*/

为了更好地理解,您能简要评论一下 internal::string_proxy<STRSXP>&String 的关系以及为什么不能使用 String 吗? - NoBackingDown
@Dominik 简而言之,它们并不是真正相关的;string_proxy基本上是一个轻量级的包装类(即代理),当单个元素在Vector中被访问时返回。这种方法使得可以为原本将是CHARSXPconst char*(可能)的东西添加功能(例如多个构造函数、运算符重载等),而不实际存储(“拥有”)一个SEXP本身。 - nrussell
由于string_proxy仅仅持有给定CharacterVector中特定元素的引用,这使得修改可以通过代理对象并影响父向量(例如通过Vector::operator[])进行传递。另一方面,String是更完整的字符串类。而string_proxy仅包含一个静态的std :: string缓冲区、一个索引和指向其父向量的指针,String包含更多数据成员,因此它将需要比等效代理对象更多的内存。 - nrussell
另外,由于String“拥有”其底层数据(即SEXP data成员),而不是持有对CHARSXP的引用,因此对String的修改将仅影响该对象本身,而不会影响其他任何东西。请注意这个示例中两个对象的行为差异。 - nrussell

7
一种可行的解决方案是使用:
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
CharacterVector concatenate(std::string x, std::string y)
{
               return wrap(x + y);
}

那么:
Vconcatenate=Vectorize(concatenate)
Vconcatenate(letters[1:2],letters[3:4])

或者:
// [[Rcpp::export]]
CharacterVector concatenate(std::vector<std::string> x,std::vector<std::string> y)
{
  std::vector<std::string> res(x.size());
  for (int i=0; i < x.size(); i++)
  {
    res[i]=x[i]+y[i];
  }
  return wrap(res);
}

为什么你说不是完全的Rcpp呢?当然它是,否则你怎么能得到glue和Rcpp::CharacterVector类型呢?但是你忘记了必需的#includeRcpp::export标签。 - Dirk Eddelbuettel
我指的是使用向量化函数。 - user3507085
1
这个粘合剂也适用于 std::vector<std::string>;然后你可以在里面循环 <耸肩>。 - Dirk Eddelbuettel
好的,我添加这个解决方案。 - user3507085

3

我将保留@nrussell提供的有关使用push_back()的警告,但仍保留此答案!


我自己还在逐渐掌握Rcpp,所以我选择在循环中使用字符串构建器。

library(Rcpp)

cppFunction('StringVector concatenate(StringVector a, StringVector b)
{
  StringVector c;
  std::ostringstream x;
  std::ostringstream y;

 // concatenate inputs
  for (int i = 0; i < a.size(); i++)
    x << a[i];

  for (int i = 0; i < b.size(); i++)
    y << b[i];

  c.push_back(x.str());
  c.push_back(y.str());

  return c;

}')

a=c("a","b"); b=c("c","d");
concatenate(a,b)
# [1] "ab" "cd" 

比较(i) 反复调用push_back与(ii) 预分配和填充策略的性能,我们可以看出后者更可取:

#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
CharacterVector pbpaste(CharacterVector lhs, CharacterVector rhs)
{
    R_xlen_t i = 0, sz = lhs.size();
    CharacterVector res;

    for (std::ostringstream oss; i < sz; i++, oss.str("")) {
        oss << lhs[i] << rhs[i];
        res.push_back(oss.str());
    }

    return res;
}

// [[Rcpp::export]]
CharacterVector sspaste(CharacterVector lhs, CharacterVector rhs)
{
    R_xlen_t i = 0, sz = lhs.size();
    CharacterVector res(sz);

    for (std::ostringstream oss; i < sz; i++, oss.str("")) {
        oss << lhs[i] << rhs[i];
        res[i] = oss.str();
    }

    return res;
}

/*** R

lhs <- as.character(1:5000); rhs <- as.character(5001:10000)

all.equal(pbpaste(lhs, rhs), sspaste(lhs, rhs))
# [1] TRUE

microbenchmark::microbenchmark(
    "push_back" = pbpaste(lhs, rhs),
    "preallocate" = sspaste(lhs, rhs),
    times = 200L
)
# Unit: milliseconds
#         expr        min         lq       mean     median         uq        max neval cld
#    push_back 101.521579 105.334649 115.156544 107.275678 110.957420 256.722239   200   b
#  preallocate   1.364213   1.585818   1.789564   1.778153   1.934758   2.955352   200   a

*/

3
如你所说,你刚接触 Rcpp,需要注意尽可能避免在 Rcpp 的 *Vector 类型中使用 push_back。Vector 类并不使用内存分配器,因此该函数比如 std::vector 中的对应函数要低效得多。当然,对于长度为两个元素的对象来说这种差异微不足道,但是对于稍大的对象来说,差异可能会很显著。 - nrussell
我认为你应该保留你的答案;除了使用 push_back 之外,你的解决方案没有任何问题。然而,你可以考虑添加另一个版本,它将结果向量 c 预分配到指定大小,然后通过使用 stringstreams 等进行单次遍历来填充它。 - nrussell
仅供参考,考虑一下我的第一个评论,即使只有一个10,000元素向量,差异有多大。 - nrussell
1
@nrussell - 是的,我明白了 :) - 我已经将你的要点添加到我的答案中,以便提醒其他人。 - SymbolixAU
3
我希望你不介意,我在你的问题中添加了一项比较,以凸显其直接适用性,因为这比我在摘要中举的人为例子更加实际。 - nrussell
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接