1.前言
上一篇博文中提到过C++/CLI典型使用场景之一是为native C++编写的算法封装接口,进而在C#项目中调用。一种典型的应用是作为Wrapper封装层来封装C++库的类和接口、做C++/C#类型的转换,然后在C#应用程序中调用,开发便捷易用的应用程序。
C++/CLI编程知识点小记_c++cli-CSDN博客文章浏览阅读1.5k次,点赞15次,收藏17次。本篇博文并非详细的C++/CLI教程,仅是博主就学习和实践总结的部分知识点记录。第一次接触C++/CLI是2017年了,用C++编写底层库,C++/CLI编写wrapper层,在C#项目中进行调用,开发应用。_c++clihttps://blog.csdn.net/baidu_38621657/article/details/142326652 C++类中经常包含字段定义,当字段为基本类型时,CLI封装类以属性(property)方式为C++类字段成员封装相对比较简单,但当字段为STL容器类型时,字段封装就比较复杂一点,其实也不算复杂,只是需要考虑一些关键点。
2.内容
CLI封装的ref class本质上是.NET框架中实现的,尽管可以使用C++/C#混合编程,可以使用native C++和managed C++,这就需要考虑一个关键点:
关键点1:
property属性get返回的字段一般可以被修改其本身的值(如其成员字段);
ref class本质上是.NET框架下的类,其遵循.NET对象管理和回收机制,包括引用类型传递和管理;
2.1基本类型
一般来说基本类型(int、float、double、array等)可以用property封装,以方便的控制和处理对应字段的读写,可参考:How to: Use Properties in C++/CLI | Microsoft LearnLearn more about: How to: Use Properties in C++/CLIhttps://learn.microsoft.com/en-us/cpp/dotnet/how-to-use-properties-in-cpp-cli?view=msvc-170
2.2 非基本类型
如果字段是非基本类型(如STL或自定义类型等),使用property封装时就要慎重了,这时要分情况处理。
2.2.1 CLI类(ref class)AWrapper是对C++类A的封装
如果CLI类(ref class)AWrapper是对C++类A的封装,其包含一个类型为A*的成员字段,封装了A类型的字段和成员函数,这时候建议不要使用property对A类型字段进行封装,因为很难满足上述关键点1的要求,如下述代码:
public ref class LineWrapper
{
public:
LineWrapper()
{
mImpl = new Line();
}
LineWrapper(const LineWrapper% other)
{
mImpl = new Line(*other.mImpl);
}
LineWrapper(const Line* impl)
{
mImpl = new Line(*impl);
}
LineWrapper(Vector3Wrapperf^ pt0, Vector3Wrapperf^ pt1)
{
mImpl = new Line();
mImpl->pt0 = *pt0->GetImplPtr();
mImpl->pt1 = *pt1->GetImplPtr();
}
~LineWrapper()
{
delete mImpl;
}
property Vector3Wrapperf^ Pt0
{
Vector3Wrapperf^ get()
{
Vector3f pt = mImpl->pt0;
return gcnew Vector3Wrapperf(pt.X, pt.Y, pt.Z);
};
void set(Vector3Wrapperf^ pt)
{
mImpl->pt0.X = pt->X;
mImpl->pt0.Y = pt->Y;
mImpl->pt0.Z = pt->Z;
};
}
property Vector3Wrapperf^ Pt1
{
Vector3Wrapperf^ get()
{
Vector3f pt = mImpl->pt1;
return gcnew Vector3Wrapperf(pt.X, pt.Y, pt.Z);
};
void set(Vector3Wrapperf^ pt)
{
mImpl->pt1.X = pt->X;
mImpl->pt1.Y = pt->Y;
mImpl->pt1.Z = pt->Z;
};
}
void SetImplPtr(Line* ptr)
{
delete mImpl;
mImpl = ptr;
}
Line* GetImplPtr()
{
return mImpl;
}
private:
Line* mImpl;
};
当我们在C#项目中写下如下代码时回发生什么行为?
var line = new LineWrapper();
line.Pt0 = new Vector3Wrapperf(100.0f,0.0f,0.0f); //和预期行为一致
line.Pt0.X = 200.0f; //和预期行为不一致!并没有成功赋值。原因:调用property Pt0 get返回gcnew 新对象,对新对象的X进行了赋值,并没有对line.Pt0的X进行赋值
第3行和预期行为不一致!并没有成功赋值。原因:调用property Pt0 get返回gcnew 新对象,对新对象的X进行了赋值,并没有对line.Pt0的X进行赋值。
如果我们尝试在ref class LineWrapper中添加成员字段Vector3Wrapperf^ mPt0,并且让类型为Vector3f的(ref class LineWrapper也是对C++类Vector3f的封装,实现手法和上述类似)mPt0->mImpl指向mImpl->pt0,这个问题会解决吗?
public ref class LineWrapper
{
public:
LineWrapper()
{
mImpl = new Line();
InitPt0();
InitPt1();
}
LineWrapper(const LineWrapper% other)
{
mImpl = new Line(*other.mImpl);
InitPt0();
InitPt1();
}
LineWrapper(const Line* impl)
{
mImpl = new Line(*impl);
InitPt0();
InitPt1();
}
LineWrapper(Vector3Wrapperf^ pt0, Vector3Wrapperf^ pt1)
{
mImpl = new Line();
mImpl->pt0 = *pt0->GetImplPtr();
mImpl->pt1 = *pt1->GetImplPtr();
InitPt0();
InitPt1();
}
~LineWrapper()
{
delete mImpl;
}
property Vector3Wrapperf^ Pt0
{
Vector3Wrapperf^ get()
{
return mPt0;
};
void set(Vector3Wrapperf^ pt)
{
mImpl->pt0.X = pt->X;
mImpl->pt0.Y = pt->Y;
mImpl->pt0.Z = pt->Z;
};
}
property Vector3Wrapperf^ Pt1
{
Vector3Wrapperf^ get()
{
return mPt1;
};
void set(Vector3Wrapperf^ pt)
{
mImpl->pt1.X = pt->X;
mImpl->pt1.Y = pt->Y;
mImpl->pt1.Z = pt->Z;
};
}
void SetImplPtr(Line* ptr)
{
delete mImpl;
mImpl = ptr;
}
Line* GetImplPtr()
{
return mImpl;
}
private:
void InitPt0()
{
mPt0 = gcnew Vector3Wrapperf();
mPt0->setImplPtr(&mImpl->pt0);
}
void InitPt1()
{
mPt1 = gcnew Vector3Wrapperf();
mPt1->setImplPtr(&mImpl->pt1);
}
private:
Line* mImpl;
Vector3Wrapperf^ mPt0; //保证mPt0内的类型为Vector3f的mImpl指针始终指向mImpl->pt0
Vector3Wrapperf^ mPt1; //同上
};
这样处理会解决部分问题,如上述第3行代码按预期行为执行,但如果property类型为List<Vector3Wrapperf^>^时,上述就会有新问题:
var test = new LineWrapper(new Vector3Wrapperf(-100.0f, -100.0f, -100.0f), new Vector3Wrapperf(100.0f, -100.0f, -100.0f)); face.OuterLoop.LstLine[0] = test; //没有按预期执行!调用property LstLine get,返回gcnew List<LineWrapper>,处理后并没有对face.OuterLoop.LstLine中位序为0的line进行赋值! face.OuterLoop.LstLine[0].Pt0.Z = 200.0f; //按预期执行
此外由于mPt0是托管对象,在LineWrapper被释放时,会执行delete mImpl;同时可能其他类型为Vector3Wrapperf的C#对象b可能指向和LineWrapper中的mPt0,这时就可能发生b还在生命周期,但已失去“行动力”的情况,因为其内部赖以生存的mImpl指向的对象已经在delete后释放了,出现了野指针情况!
建议如2.2.1节开始所述,对于非基本类型使用下述函数而不使用property,如下述代码。
public ref class LineWrapper
{
public:
LineWrapper()
{
mImpl = new Line();
}
LineWrapper(const LineWrapper% other)
{
mImpl = new Line(*other.mImpl);
}
LineWrapper(const Line* impl)
{
mImpl = new Line(*impl);
}
LineWrapper(Vector3Wrapperf^ pt0, Vector3Wrapperf^ pt1)
{
mImpl = new Line();
mImpl->pt0 = *pt0->GetImplPtr();
mImpl->pt1 = *pt1->GetImplPtr();
}
~LineWrapper()
{
delete mImpl;
}
void setPt0(Vector3Wrapperf^ pt)
{
mImpl->pt0.X = pt->X;
mImpl->pt0.Y = pt->Y;
mImpl->pt0.Z = pt->Z;
}
Vector3Wrapperf^ getPt0()
{
return gcnew Vector3Wrapperf(mImpl->pt0.X, mImpl->pt0.Y, mImpl->pt0.Z);
}
void setPt1(Vector3Wrapperf^ pt)
{
mImpl->pt1.X = pt->X;
mImpl->pt1.Y = pt->Y;
mImpl->pt1.Z = pt->Z;
}
Vector3Wrapperf^ getPt1()
{
return gcnew Vector3Wrapperf(mImpl->pt1.X, mImpl->pt1.Y, mImpl->pt1.Z);
}
void SetImplPtr(Line* ptr)
{
delete mImpl;
mImpl = ptr;
}
Line* GetImplPtr()
{
return mImpl;
}
private:
Line* mImpl;
};
2.2.2 CLI类(ref class)AWrapper不依赖C++类A实现
CLI类(ref class)AWrapper和C++类A没有直接关联,相关的算法需要进行AWrapper和A之间的转换,这种情况就像写C#类,对于基本类型和非基本类型都可以正常使用property进行封装,这里不再赘述。
3.总结
用C++/CLI对C++算法进行封装时可以有两种方案:
- 对需要封装的C++类进行CLI封装,包含类型为对应C++类型指针的成员字段mImpl,对非基本类型不使用property封装,而使用函数封装,见上文;
- 如2.2.2节所属,C++/CLI中定义不依赖C++类型的Wrapper类,而在相关的算法封装中实现好C++和CLI类型的转换即可,如C++ class Line和CLI ref class LineWrapper中都有pt0、pt1字段,两class没有直接关系,在C++/CLI算法封装中进行两类变量的转换即可,如:
- )将C#传递过来的CLI变量转为C++类型变量;
- )然后调用C++算法接口,获取类型为C++类型的结果变量;
- )将类型为C++类型的结果变量转为CLI类型的变量返回给C#调用;
- )在C#中正常组织参数调接口,获取结果,使用即可;
图形几何、数据处理、并行计算相关研究和研发,公众号:geometrylib,欢迎交流。