65.9K
CodeProject 正在变化。 阅读更多。
Home

注册指针 - 可以定位堆栈的高性能 C++ 智能指针

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (6投票s)

2016年3月11日

MIT

6分钟阅读

viewsIcon

22930

downloadIcon

182

介绍一种新的智能指针,旨在成为原生指针和(原生)引用的安全替代品。

快速摘要

mse::TRegisteredPointer 是一种智能指针,其行为与原生指针几乎完全相同,不同之处在于,当目标对象被销毁时,它的值会自动设置为 null_ptr。在大多数情况下,它可以作为原生指针的通用替代品。与原生指针一样,它本身没有线程安全特性。但作为回报,它没有问题指向在栈上分配的对象(并获得相应的性能优势)。当默认的运行时检查启用时,这种指针可以防止访问无效内存。

 

mse::TRegisteredFixedPointermse::TRegisteredPointer 的一个派生类,在功能上等同于 C++ 引用。也就是说,它只能被构造为指向一个已存在的对象,并且在构造后不能被重新定向。虽然这些特性可能使得 C++ 引用不太可能最终被用于访问无效内存,但这并非不可能。另一方面,mse::TRegisteredFixedPointer 继承了 mse::TRegisteredPointer 关于无效内存访问的安全性。

 

谁应该使用注册指针?

注册指针适用于两类 C++ 开发人员 - 一类是安全性和安全性至关重要的人,以及其他所有人。
注册指针可以帮助消除许多意外访问无效内存的机会。
虽然使用注册指针可能会带来轻微的性能成本,但由于注册指针在指向有效对象时与原生指针的行为相同,因此可以使用编译时指令将它们“禁用”(自动替换为相应的原生指针),从而允许在调试/测试/Beta 版本中使用它们来帮助捕获 bug,而在发布版本中则不会产生任何开销。所以,真的没有理由不使用它们。
 

用法

使用注册指针非常简单。只需将两个文件,mseprimitives.hmseregistered.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 = &registered_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 = &registered_a;
    rp<A> A_registered_ptr2 = rnew<A>();
    rdelete<A>(A_registered_ptr2);

本文附带的示例项目包含了一套关于注册指针实际应用的全面示例。

 

讨论

如今,C++ 作为一种语言,其危险性尤为突出。至少与其他现代语言相比是如此。我所说的“危险”,是指始终存在的访问无效内存的重大可能性。无效内存访问的潜在后果可能非常严重,从敏感数据泄露到运行环境被完全攻破。

这可能是 C++ 不受(服务器端)Web 应用程序欢迎的主要原因。然而,奇怪的是,它仍然是 Web 基础架构关键部分所使用的语言。例如,Web 服务器和 Web 浏览器。为什么会这样?我认为这仅仅是因为没有其他语言真正胜任这项工作。一个特别的问题是,许多其他语言依赖垃圾回收来实现语言安全性,而这对于编写需要可靠响应的复杂系统来说可能并不合适。

但 C++ 仍然很危险,并且有无数的安全漏洞利用了这一点。

自 C++11 以来,C++ 已成为一种功能强大的语言。是否真的没有实用方法可以避免使用 C++ 的危险元素?嗯,让我们考虑最危险的元素——指针。经验丰富的(年长的)C++ 程序员知道,无意中让指针指向无效内存是多么容易。现在情况有所好转,因为 STL 提供了许多常用动态数据结构的经过充分测试的版本,这样您就不必自己实现它们,从而消除了对使用指针的需求。

并且在使用动态分配时,std::shared_ptr 通常可以很好地替代原生指针,有助于确保您不会意外地过早释放目标对象。使用 std::shared_ptr 基本上可以获得垃圾回收的安全优势,但与垃圾回收一样,这会带来性能成本。在我看来,在几乎所有情况下,安全优势都是值得的,但其他人可能会有不同意见。

C++ 社区中的普遍观点是,在用户不拥有目标对象(即不安排其销毁)的情况下使用原生指针仍然是合适的。更精明的程序员会加上一个条件,即您必须确定目标对象比指针的生命周期长。问题是这个条件很容易出错。考虑以下示例:

#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. */
}

add_quarantine_patient()》函数的作者可能从未想过,对新患者的引用也可能是一个主管医生的引用,在这种情况下,该函数可能会在它完成使用之前无意中导致其 p_patient_name 参数的目标失效。

这是一个牵强的例子,但这类事情在更复杂的情况下很容易发生。当然,在绝大多数情况下,使用原生指针是完全安全的。问题在于,在少数情况下,很容易认为它是安全的,但实际上并非如此。因此,稳妥的做法是干脆不使用原生指针(除非您要做一些非常彻底的测试)。

同样,用 std::shared_ptr 替换原生指针将是一个简单的解决方案,但会带来性能成本。其中很大一部分性能成本来自于 std::shared_ptr 目标对象不能(或不应)在栈上分配的限制。因此,在考虑性能时,注册指针通常是更好的选择。

这是上面示例在用注册指针替换原生指针(和引用)后的样子:

#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 的性能轻松超过了即使是指向堆上分配对象的原生(即原始)指针。

就这样。大家安全编码。

 

© . All rights reserved.