R与Cpp混合编程一
Last update: 2021-06-09
Rcpp基本简介
Armadillo supports matrices with the following element types: float, double, std::complex
R配置
要正确编译cpp文件,R的安装路径不能有空格,否则会报错如下:
Error in system(cmd, intern = !showOutput) : 'C:/Program' not found
基本介绍
Rcpp是R与C++混合编程的基础接口包,高效计算的R包中大部分代码都不是用R写的,其中C++混合就是实现高效计算的一个好办法。Armadillo当之无愧地是线性代数计算中R与C++混合编程优秀的接口包,为广大科研工作者所常用。
定义C++函数时加vec& x是引用时定义了一个local variable,然后不是传输整个数据到另一个函数中,这样能加快计算速度。这一种思想就是传说中的引用传递,它比指针传递简单,比值传递高效。
h文件和hpp文件常在cpp的脚本开头出现,它是实现调用现有的C++库函数库函数的方法。我们在写R包时常常把多个Cpp文件用到的公共C++函数定义放到单独的一个文件中,然后再用一个hpp头文件去申明这些函数,最后需要调用它们的Cpp脚本中inlude进去便可以使用。
R与Cpp混合编程有许多trick需要格外注意,初学者常常会遭遇许多的坑。在此,我将我遇到的坑都总结下来,希望减少后来者走弯路的可能性。
switch语句
C++中switch的使用需要注意的几点: 只能针对基本数据类型中的整型类型使用switch,这些类型包括int、char等。对于其他类型,则必须使用if语句。 switch()的参数类型不能为实型 。 case标签必须是常量表达式(constantExpression),如42或者’4’。 case标签必须是惟一性的表达式;也就是说,不允许两个case具有相同的值。
Rcpp加速计算
- Armadillo中: it is faster to use inv_sympd() instead than inv or .i(). log_det_sympd is faster than log_det.
- 有返回值的中间函数,改成void,无返回值,计算更快
- 将一个矩阵乘以一个对角矩阵转化为逐元相乘,降低计算复杂度。 比如一个np的矩阵乘以一个pp的对角矩阵,计算复杂度为O(np^2),而用逐元相乘则降低为O(np);当p非常高的时候,能大大降低计算复杂度。
- 提前缓存好不需要更新迭代的变量。
- 将double换成float类型。
强大的引用传递
引用专递就是传递对象本身,值传递是传递对象的复制品。 打个比方:比如你有一张相片要修改,把相片编辑工具看作函数,把被处理的相片看作参数,那么直接操作相片原件就相当于引用传递,把相片复制一份然后操作这个复制品就叫值传递。
很明显引用传递会影响当作参数的对象,而值传递不会影响当作参数的对象 C++语言中,函数的参数和返回值的传递方式有三种: 值传递、指针传递和引用传递。
以下是“值传递”的示例程序。
由于Func1函数体内的x是外部变量n的一份拷贝,改变x的值不会影响n, 所以n的值仍然是0。
void Func1(int x)
{
x = x + 10;
}
...
int n = 0;
Func1(n);
cout << "n = " << n << endl; // n = 0
以下是“指针传递”的示例程序。 由于Func2函数体内的x是指向外部变量n的指针,改变该指针的内容将导致n的值改变,所以n的值成为10。
void Func2(int *x)
{
(* x) = (* x) + 10;
}
...
int n = 0;
Func2(&n);
cout << "n = " << n << endl; // n = 10
以下是“引用传递”的示例程序。 由于Func3函数体内的x是外部变量n的引用,x和n是同一个东西,改变x等于改变n,所以n的值成为10。
void Func3(int &x)
{
x = x + 10;
}
...
int n = 0;
Func3(n);
cout << "n = " << n << endl; // n = 10
对比上述三个示例程序,会发现“引用传递”的性质象“指针传递”,而书写方式象“值传递”。 实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西? 答案是“用适当的工具做恰如其分的工作”。 指针能够毫无约束地操作内存中的任何东西,尽管指针功能强大,但是非常危险。 如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外
c++中使用引用传递来提高效率
我们写一个函数,比如 objclass fun(objclass obj); objclass是类名,obj是对象,fun是函数名。
然后调用此函数,编译器分两个步骤:
1.每次通过值传递的方式给函数传递一个对象时,都会建立一个该对象的拷贝。
2.每次通过值从函数返回一个对象时,也会建立另一个拷贝。
也就是说调用一次此函数,系统会自动建立两次的对象拷贝,然后再调用两次析构函数释放对象的拷贝。
我们知道当调用函数的时候,这些对象被复制到栈中,这样做会费时且占用内存,当使用用户定义的大对象时,这种拷贝带来的内存开销是非常显著的。当时用用户建立的类时,每次生成这种临时的拷贝都要调用一个特殊的构造函数:复制构造函数。当函数返回时,对象的临时拷贝被删除,此时需要调用对象的析构函数。如果返回对象时通过值传递的方式,那么必须建立对象的拷贝,然后再删除。对于很大的对象,调用构造函数和析构函数在速度和内存方面都会造成很大的开销。
1 尽量减少值传递,多用引用来传递参数。
5 ++i和i++引申出的效率问题
8 减少除法运算的使用
9 将小粒度函数声明为内联函数(inline)
10 与直接初始化对应的是复制初始化,什么是直接初始化?什么又是复制初始化?
头文件的意义
Cpp中有两类文件,一个是cpp源文件.cpp为扩展名,用于写源代码,包括变量和函数定义;一个是头文件.h为扩展名,用于申明变量和函数,不能包含变量和函数的定义。 只能在头文件中写形如:extern int a;和void f();的句子。这些才是声明。如果写上int a;或者void f() {}这样的句子,那么一旦这个头文件被两个或两个以上的.cpp文件包含的话,编译器会立马报错。 “extern int a;”表示申明一个已定义的变量;“void f();”表示申明一个已定义的无返回值的函数f。 但是,有三种情况除外,头文件中可以写const和static对象的定义;头文件中可 以写内联函数(inline)的定义;头文件中可以写类 (class)的定义。
申明函数
头文件中申明函数的语法:
int Dhv2Feature(unsigned char* p_ucData, int iWidth, int iHeight, unsigned short* p_usBlock);
float Dhv2Dhv(unsigned short* sF1, unsigned short* sF2, int iWidth, int iHeight);
另外省略参数名也是可以的:
int Dhv2Feature(unsigned char* , int , int , unsigned short* );
float Dhv2Dhv(unsigned short* , unsigned short* , int , int );
申明变量
头文件中申明函数的语法:
extern int a;
类对象的申明
car.h:
#ifndef __Car_H__
#define __Car_H__
class Wheel;
class Car
{
public:
Car(int num);
~Car();
void show();
private:
Wheel* mWheel;
int mWheelNum;
};
#endif
wheel.h:
#ifndef __Wheel_H__
#define __Wheel_H__
#include <iostream>
using namespace std;
class Wheel
{
public:
Wheel();
~Wheel();
void show();
};
#endif
定义wheel.cpp
#include "Wheel.h"
Wheel::Wheel()
{
cout << "Wheel is created" << endl;
}
Wheel::~Wheel()
{
cout << "Wheel is destroyed" << endl;
}
void Wheel::show()
{
}
如果加入了命名空间,怎么写类声明呢? 1.如果类都在同一命名空间,那直接写在同一namespace里面就可以了
#ifndef __Wheel_H__
#define __Wheel_H__
#include <iostream>
using namespace std;
namespace Gnuser
{
class Wheel
{
public:
Wheel();
~Wheel();
void show();
};
}
#endif
#ifndef __Car_H__
#define __Car_H__
namespace Gnuser
{
class Wheel;
class Car
{
public:
Car(int num);
~Car();
void show();
private:
Wheel* mWheel;
int mWheelNum;
};
}
#endif
- 现在如果再加一个命名空间,那么可以这样定义
#ifndef __Road_H__
#define __Road_H__
#include <vector>
using namespace std;
namespace Gnuser
{
class Car;
}
namespace Dxn
{
class Road
{
public:
Road(int num);
~Road();
void addCar();
private:
vector<Gnuser::Car*> mCars;
};
}
#endif
预防头文件的反复调用
在C++语言编程中,我们经常会接触到头文件,比如说声明类,或者声明命名空间等,而每次在编写xxx.h的头文件时,编程书上都会让我们在代码的前后加上如下的三句代码:
#ifndef XXX_H_
#define XXX_H_
……
#endif
比如“#ifndef Rcpp__sugar__sets_h”表示判断Rcpp文件夹的子文件sugar中的sets.h头文件是否已经被包含,若已经包含进去,则不执行“……”中的申明语句。
更多详情见: https://blog.csdn.net/u012617944/article/details/78405686; https://blog.csdn.net/leowinbow/article/details/82884518;
头文件与同名源文件的关系
头文件和源文件不必同名,只是约定俗成的,为了好记而已。 见 https://blog.csdn.net/lee244868149/article/details/39341751
hpp头文件与h头文件的区别 C++中的.hpp文件
见 https://blog.csdn.net/acoolgiser/article/details/102832675
头文件中的功能函数
Cpp头文件的功能函数可以去网站查询其具体用法: http://www.cplusplus.com/reference/cmath/exp/
命名空间详解
见: https://zhuanlan.zhihu.com/p/126481010 C++中加const与不加const的区别:https://www.cnblogs.com/code-fisher/p/5329026.html
编程注意
易犯错误
- 最大值最小值溢出,C++中最小值。非常接近0的溢出为0。比如\(4^{-200}*A %( 2* 4^{-200}*B)\),对于这种就需要先把\(4^{-200}\)约掉后再计算两个矩阵的逐元除法。
2)整除错误:C++中整数除整数等于相除之后再向下取整。如下示例代码:
U0 = (bx - 1/n *A * tA * a) / as_scalar(1- 1/n * a.t()*tA * a);
## 这里1/n=0 而不等于真正的小数。应该修改成:
U0 = (bx - 1.0/n *A * tA * a) / as_scalar(1- 1.0/n * a.t()*tA * a);
这是我找了大半天的错误才找到错误的原因!气死我了,好多的坑。我知道这样C++有这个语法规则,但是写代码的时候还是容易疏忽,而且还找不到错误的点在哪里。
- 公式中的加号运算错误的写成减法运算,导致出错。这是我容易出错的地方。 例如:
X_tk = X - repmat(U0.t() + Mu0.row(k)* W0.t(), n, 1);
#错误写成:
X_tk = X - repmat(U0.t() - Mu0.row(k)* W0.t(), n, 1);
- 避免log(a)当a=0报错的情况,可以使用
log(a + (a==0)) # a=0时,值会等于0
函数定义
- 可变虚参定义无返回值函数:这里可变虚参指函数的输入虚参数,通过 “type& object”形式使得传入该函数的变量具有可变性质。于是使得无返回值函数具有了返回值函数的能力。例如:
void changeValue(int x){ //若不加取地址符号
x = x + 1;
}
int y = 1;
changeValue(y); // 只能改变虚参的值,实参变量的值不变。
cout>> y;
// 1
void changeValue(int& x){
x = x + 1;
}
int y = 1;
changeValue(y); //加上取地址符号后,就可以改变实参变量的值了。
cout>> y; //arma::svd函数就是一个典型的例子。
// 2
- 函数返回值为列表的方法:
# 1.
List output;
output["alpha"] = alpha;
output["beta0"] = beta0;
# 2.
List output = List::create(Rcpp::Named("alpha") = alpha,
Rcpp::Named("alpha0") = alpha0,
Rcpp::Named("beta0") = beta0,
Rcpp::Named("sigma2y") = sigma2y,
Rcpp::Named("sigma2z") = sigma2z,
Rcpp::Named("sigma2beta") = sigma2beta,
Rcpp::Named("gam") = gam,
Rcpp::Named("loglik_seq") = loglik_out,
Rcpp::Named("loglik") = loglik_max,
Rcpp::Named("iteration") = Iteration-1);
- 字符串划分函数的使用 利用boost.h头文件中的split()函数来划分字符串,它是一个可变实参变量的无返回值函数。
Example:
Input : boost::split(result, input, boost::is_any_of("\t"))
input = "geeks\tfor\tgeeks"
Output : geeks
for
geeks
Explanation: Here in input string we have "geeks\tfor\tgeeks"
and result is a container in which we want to store our result
here separator is "\t".
具体代码:
/ C++ program to split
// string into substrings
// which are separated by
// separater using boost::split
// this header file contains boost::split function
#include <bits/stdc++.h>
#include <boost/algorithm/string.hpp>
using namespace std;
int main()
{
string input("geeks\tfor\tgeeks");
vector<string> result;
boost::split(result, input, boost::is_any_of("\t"));
for (int i = 0; i < result.size(); i++)
cout << result[i] << endl;
return 0;
}
更多见: https://www.geeksforgeeks.org/boostsplit-c-library/
字符串转化为数值型atof atof是iostream.h头文件中命名空间std中的函数,它是ascII to float的缩写,它将ascII字符串转换为相应的单精度浮点数,比如传入“1.234”,经过处理后就返回float类型的数1.234 。类似的还有atoi 、atol、itoa、ftoa等等。
指针和指针变量
int a ; //定义int类型变量
int *p = &a; //变量 p 是一个 int* 类型的一级指针变量,&是取地址符,p保存了a 的地址
cout << *p <<endl; //输出 p 指向变量的值,即输出a的值
cout << p << endl; //输出 p 的值,即输出变量a在内存中的地址
int **q; //定义二级指针变量
q = &p; // 二级指针变量q保存了一级指针变量p的地址
cout << q <<endl; //输出指针变量p在内存中的地址
cout << *q << endl; //输出q指向变量的值,即变量p的值,即a的地址
cout << **q << endl; //可以这样理解 cout<<*(*q), 等价于 cout <<*p, 即输出a的值
https://blog.csdn.net/woainilixuhao/article/details/81784124
- 时间变量 std命名空间中还有时间类型. std::clock_t a = clock();
快速调用R中现有函数
以glmnet包中的函数为例:
List cv_glmnet(mat x, vec y, double alpha){
Rcpp::Environment stats("package:stats");
Rcpp::Function optim = stats["optim"];
// calling cv.glmnet()
Environment glmnet("package:glmnet");
Function f = glmnet["cv.glmnet"];
return f(Named("x") = x, Named("y") = y, Named("alpha") = alpha);
}
具体代码:
library(glmnet)
res <- cv_glmnet(cbind(1:10,1), 1:10, 0.1)
res$lambda.min
控制输出精度
// Program 2.8 Experimenting with floating point output
#include <iostream> // fixed scientific
#include <iomanip> // setprecision()
using std::setprecision;
using std::fixed;
using std::scientific;
using std::cout;
using std::endl;
int main() {
float value1 = 0.1f;
float value2 = 2.1f;
value1 -= 0.09f; // Should be 0.01
value2 -= 2.09f; // Should be 0.01
cout << value1 - value2 << endl;
cout << setprecision(14) << fixed; // Change to fixed notation
cout << value1 - value2 << endl; // Should output zero
cout << setprecision(2) << scientific; // Set scientific notation
cout << value1 - value2 << endl; // Should output zero
return 0;
}