注册指针 - 可以定位堆栈的高性能 C++ 智能指针
介绍一种新的智能指针,旨在成为原生指针和(原生)引用的安全替代品。
快速摘要
mse::TRegisteredPointer 是一种智能指针,其行为与原生指针几乎完全相同,不同之处在于,当目标对象被销毁时,它的值会自动设置为 null_ptr。在大多数情况下,它可以作为原生指针的通用替代品。与原生指针一样,它本身没有线程安全特性。但作为回报,它没有问题指向在栈上分配的对象(并获得相应的性能优势)。当默认的运行时检查启用时,这种指针可以防止访问无效内存。
mse::TRegisteredFixedPointer 是 mse::TRegisteredPointer 的一个派生类,在功能上等同于 C++ 引用。也就是说,它只能被构造为指向一个已存在的对象,并且在构造后不能被重新定向。虽然这些特性可能使得 C++ 引用不太可能最终被用于访问无效内存,但这并非不可能。另一方面,mse::TRegisteredFixedPointer 继承了 mse::TRegisteredPointer 关于无效内存访问的安全性。
谁应该使用注册指针?
注册指针适用于两类 C++ 开发人员 - 一类是安全性和安全性至关重要的人,以及其他所有人。
注册指针可以帮助消除许多意外访问无效内存的机会。
虽然使用注册指针可能会带来轻微的性能成本,但由于注册指针在指向有效对象时与原生指针的行为相同,因此可以使用编译时指令将它们“禁用”(自动替换为相应的原生指针),从而允许在调试/测试/Beta 版本中使用它们来帮助捕获 bug,而在发布版本中则不会产生任何开销。所以,真的没有理由不使用它们。
用法
使用注册指针非常简单。只需将两个文件,mseprimitives.h 和 mseregistered.h,复制到你的项目(或“include”目录)中。没有其他依赖项。注册指针的使用方式与原生指针非常相似,它们通常可以作为“即插即用”的替代品。请注意,目标对象必须声明为一个“注册对象”。由于注册对象类型从原始对象类型公开派生,因此它与其保持兼容。
#include "mseregistered.h"
...
class A {
public:
int b = 3;
};
A a;
mse::TRegisteredObj<A> registered_a;
A* A_native_ptr1 = &a;
mse::TRegisteredPointer<A> A_registered_ptr1 = ®istered_a;
A* A_native_ptr2 = new A();
mse::TRegisteredPointer<A> A_registered_ptr2 = mse::registered_new<A>();
delete A_native_ptr2;
mse::registered_delete<A>(A_registered_ptr2);
#include "mseregistered.h"
using namespace mse;
...
class A {
public:
int b = 3;
};
ro<A> registered_a;
rp<A> A_registered_ptr1 = ®istered_a;
rp<A> A_registered_ptr2 = rnew<A>();
rdelete<A>(A_registered_ptr2);
本文附带的示例项目包含了一套关于注册指针实际应用的全面示例。
讨论
#include <vector>
class CNames : public std::vector<std::string> {
public:
void addName(const std::string& name) {
(*this).push_back(name);
}
};
class CQuarantineInfo {
public:
void add_quarantine_patient(const std::string* p_patient_name) {
if (p_patient_name) {
if ((3 * supervising_doctors.size()) <= quarantined_patients.size()) {
/* The policy is to have at least one supervising doctor for every 3 patients. */
if (1 <= available_reserve_doctors.size()) {
supervising_doctors.addName(available_reserve_doctors.back());
supervising_doctors.shrink_to_fit(); /* Just to increase the likelihood of exposing
the bug. */
available_reserve_doctors.pop_back();
}
}
quarantined_patients.addName(*p_patient_name);
}
}
CNames quarantined_patients;
CNames supervising_doctors;
CNames available_reserve_doctors;
};
void main(int argc, char* argv[]) {
CQuarantineInfo quarantine_info;
quarantine_info.available_reserve_doctors.addName("Dr. Bob");
quarantine_info.available_reserve_doctors.addName("Dr. Dan");
quarantine_info.available_reserve_doctors.addName("Dr. Jane");
quarantine_info.available_reserve_doctors.addName("Dr. Tim");
quarantine_info.add_quarantine_patient(&std::string("Amy"));
quarantine_info.add_quarantine_patient(&std::string("Carl"));
quarantine_info.add_quarantine_patient(&std::string("Earl"));
/* Suppose the supervising doctor contracts the infection and becomes a patient too. */
const std::string* p_name_of_doctor_that_contracted_the_infection = &(quarantine_info.supervising_doctors.front());
quarantine_info.add_quarantine_patient(p_name_of_doctor_that_contracted_the_infection);
/* The problem here is that the add_quarantine_patient() function might first add another doctor to
the set of supervising_doctors. But because supervising_doctors is ultimately implemented as an
std::vector<>, an insert (or push_back) operation could cause a "reallocation" event which would
invalidate any references to any member of the vector. So the add_quarantine_patient() function
could inadvertently invalidate its parameter before it is finished using it. */
}
#include <vector>
#include "mseregistered.h"
using namespace mse;
/* Note that "ro<>" is aliased to mse::RegisteredObj<>, "rcp<>" to mse::RegisteredConstPointer<> and
"rfcp<>" to mse::RegisteredFixedConstPointer<>. */
class CNames : public std::vector<ro<std::string>> {
public:
void addName(rfcp<std::string> p_name) {
(*this).push_back(*p_name);
}
};
class CQuarantineInfo {
public:
void add_quarantine_patient(rcp<std::string> p_patient_name) {
if (p_patient_name) {
if ((3 * supervising_doctors.size()) <= quarantined_patients.size()) {
/* The policy is to have at least one supervising doctor for every 3 patients. */
if (1 <= available_reserve_doctors.size()) {
supervising_doctors.addName(&available_reserve_doctors.back());
supervising_doctors.shrink_to_fit(); /* Just to increase the likelihood of exposing the bug. */
available_reserve_doctors.pop_back();
}
}
quarantined_patients.addName(&*p_patient_name);
}
}
CNames quarantined_patients;
CNames supervising_doctors;
CNames available_reserve_doctors;
};
void main(int argc, char* argv[]) {
CQuarantineInfo quarantine_info;
quarantine_info.available_reserve_doctors.addName(&ro<std::string>("Dr. Bob"));
quarantine_info.available_reserve_doctors.addName(&ro<std::string>("Dr. Dan"));
quarantine_info.available_reserve_doctors.addName(&ro<std::string>("Dr. Jane"));
quarantine_info.available_reserve_doctors.addName(&ro<std::string>("Dr. Tim"));
quarantine_info.add_quarantine_patient(&ro<std::string>("Amy"));
quarantine_info.add_quarantine_patient(&ro<std::string>("Carl"));
quarantine_info.add_quarantine_patient(&ro<std::string>("Earl"));
/* Suppose the supervising doctor contracts the infection and becomes a patient too. */
rcp<std::string> p_name_of_doctor_that_contracted_the_infection = &(quarantine_info.supervising_doctors.front());
try {
quarantine_info.add_quarantine_patient(p_name_of_doctor_that_contracted_the_infection);
/* The problem here is that the add_quarantine_patient() function might first add another
doctor to the set of supervising_doctors. But because supervising_doctors is ultimately
implemented as an std::vector<>, an insert (or push_back) operation could cause a
"reallocation" event whichwould invalidate any references to any member of the vector. So the
add_quarantine_patient() function could inadvertently invalidate its parameter before it is
finished using it. */
/* By default, registered pointers will throw an exception on any attempt to access invalid
memory. */
}
catch (...) {
/* Whether the bug is exposed depends on the implementation of std::vector<>. Under msvc2015 in
debug mode (March 2016), the bug does manifest and an exception is caught here. */
}
/* Just to demonstrate that registered pointers also support stack allocated objects. */
ro<std::string> patient_fred("Fred");
quarantine_info.add_quarantine_patient(&patient_fred);
}
默认情况下,注册指针在任何尝试访问无效内存的操作时都会抛出异常。
所以,你看,C++ 最危险的元素变得安全了。而且没有牺牲栈分配的性能优势。与“SaferCPlusPlus”库的其他部分一起使用,现在就可以编写 C++ 代码,大大降低访问无效内存的风险,而且是切实可行的。
在我们结束之前,每个好的数据类型插件文章都需要一张基准测试图。
分配、释放、指针复制和赋值:
指针类型 | 时间 |
---|---|
mse::TRegisteredPointer (栈) | 0.027 秒 |
原生指针 (堆) | 0.049 秒 |
mse::TRegisteredPointer (堆) | 0.074 秒 |
std::shared_ptr (堆) | 0.087 秒 |
因此,正如我们所见,指向栈上分配对象的 mse::TRegisteredPointer 的性能轻松超过了即使是指向堆上分配对象的原生(即原始)指针。
就这样。大家安全编码。