使用 observer_ptr

Use of observer_ptr

库基础技术规范 V2 中构造 std::observer_ptr 的确切意义是什么?

在我看来,它所做的只是包装一个裸体 T*,如果它没有增加动态内存安全性,这似乎是一个多余的步骤。

在我的所有代码中,我都使用 std::unique_ptr where I need to take explicit ownership of an object and std::shared_ptr 来共享对象的所有权。

这非常有效,可以防止意外取消引用已销毁的对象。

当然,

std::observer_ptr 不保证所观察对象的生命周期。

如果它是从 std::unique_ptrstd::shared_ptr 构造的,我会看到在这样的结构中的用途,但任何仅使用 T* 的代码可能只是去继续这样做,如果他们计划转移到任何地方,那将是 std::shared_ptr and/or std::unique_ptr(取决于用途)。


给出一个简单的示例函数:

template<typename T>
auto func(std::observer_ptr<T> ptr){}

如果它阻止智能指针在被观察时销毁它们存储的对象,那将会很有用。

但是如果我想观察 std::shared_ptrstd::unique_ptr 我必须写:

auto main() -> int{
    auto uptr = std::make_unique<int>(5);
    auto sptr = std::make_shared<int>(6);
    func(uptr.get());
    func(sptr.get());
}

这使得它不比以下更安全:

template<typename T>
auto func(T *ptr){}

那么,这个新结构有什么用呢?

是否仅用于自文档源?

是否仅用于源代码自文档?

是的。

proposal 非常清楚地表明它仅用于自我记录:

This paper proposes observer_ptr, a (not very) smart pointer type that takes no ownership responsibility for its pointees, i.e., for the objects it observes. As such, it is intended as a near drop-in replacement for raw pointer types, with the advantage that, as a vocabulary type, it indicates its intended use without need for detailed analysis by code readers.

当您需要共享访问权限但不需要共享所有权.

问题是 原始指针 仍然非常有用,并且具有非常可观的用例场景。

原始指针 智能指针 管理时,它的清理是有保证的,因此,在 [=46] 的生命周期内=]智能指针,通过智能指针管理的原始指针访问实际数据是有意义的。

因此,当我们创建函数时,通常采用原始指针,保证函数不会删除该指针的一个好方法是使用强类型 class,如 std::observer_ptr

将托管 原始指针 作为参数传递给 std::observer_ptr 函数参数时,我们知道该函数不会 delete 它。

这是函数表示 "give me your pointer, I will not meddle with its allocation, I will just use it to observe".

的一种方式

顺便说一下,我不太喜欢这个名字 std::observer_ptr,因为这意味着你只能看但不能摸。但事实并非如此。我会选择更像 access_ptr.

的东西

补充说明:

这是与 std::shared_ptr 不同的用例。 std::shared_ptr 是关于共享 所有权 的,它应该 在您无法确定哪个 拥有对象时使用 =66=] 将首先超出范围。

另一方面,std::observer_ptr 适用于您想要共享 访问权限 而不是 所有权 的情况。

使用std::shared_ptr只是为了共享访问权限是不太合适的,因为那样效率很低。

因此,无论您是使用 std::unique_ptr 还是 std::shared_ptr 来管理 目标指针 ,[=46= 仍然有一个用例]raw-pointers 因此 std::observer_ptr.

的合理性

在原始指针上使用 std::observer_ptr 的一个好处是,它提供了一种更好的替代方法,可以替代从 C 继承的令人困惑且容易出错的多指针实例化语法。

std::observer_ptr<int> a, b, c;

是对

的改进
int *a, *b, *c;

从 C++ 的角度来看有点奇怪,很容易被误认为是

int* a, b, c;

是的,std::observer_ptr 的要点主要是 "self-documentation",这本身就是一个有效的目的。但应该指出的是,可以说它在这方面做得并不好,因为 "observer" 指针到底是什么并不明显。首先,正如 Galik 指出的那样,对某些人来说,这个名称似乎暗示承诺不修改目标,这不是本意,因此 access_ptr 这样的名称会更好。其次,如果没有任何限定词,该名称将暗示对其 "non-functional" 行为的认可。例如,人们可能认为 std::weak_ptr 是一种 "observer" 指针。但是 std::weak_ptr 通过提供一种允许尝试访问(已解除分配的)对象安全失败的机制来适应指针超过目标对象的情况。 std::observer_ptr 的实现不适应这种情况。所以也许 raw_access_ptr 会是一个更好的名字,因为它会更好地表明它的功能缺点。

那么,正如您有理由问的那样,这个在功能上受到挑战的 "non-owning" 指针有什么意义?主要原因可能是性能。许多 C++ 程序员认为 std::share_ptr 的开销太高,因此在需要 "observer" 指针时只会使用原始指针。提议的 std::observer_ptr 试图以可接受的性能成本提供代码清晰度的小幅改进。具体来说,零性能成本。

不幸的是,似乎有一种普遍但在我看来不切实际的乐观态度,认为将原始指针用作 "observer" 指针是多么安全。特别是,虽然很容易声明目标对象必须比 std::observer_ptr 更长寿的要求,但要绝对确定它得到满足并不总是那么容易。考虑这个例子:

struct employee_t {
    employee_t(const std::string& first_name, const std::string& last_name) : m_first_name(first_name), m_last_name(last_name) {}
    std::string m_first_name;
    std::string m_last_name;
};

void replace_last_employee_with(const std::observer_ptr<employee_t> p_new_employee, std::list<employee_t>& employee_list) {
    if (1 <= employee_list.size()) {
        employee_list.pop_back();
    }
    employee_list.push_back(*p_new_employee);
}

void main(int argc, char* argv[]) {
    std::list<employee_t> current_employee_list;
    current_employee_list.push_back(employee_t("Julie", "Jones"));
    current_employee_list.push_back(employee_t("John", "Smith"));

    std::observer_ptr<employee_t> p_person_who_convinces_boss_to_rehire_him(&(current_employee_list.back()));
    replace_last_employee_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list);
}

replace_last_employee_with() 函数的作者可能从未想过,对新员工的引用也可能是对要替换的现有员工的引用,在这种情况下,该函数可能会无意中导致其 std::observer_ptr<employee_t> 参数的目标在使用完之前被释放。

这是一个人为的例子,但这种事情在更复杂的情况下很容易发生。当然,在绝大多数情况下使用原始指针是绝对安全的。问题在于,在少数情况下,很容易假设它是安全的,但实际上并非如此。

如果用 std::shared_ptrstd::weak_ptr 替换 std::observer_ptr<employee_t> 参数出于某种原因是不可接受的,现在有另一个安全的选择 - 这是无耻的插头部分回答 - “registered pointers". "registered pointers" are smart pointers that behave just like raw pointers, except that they are (automatically) set to null_ptr when the target object is destroyed, and by default, will throw an exception if you try to access an object that has already been deleted. They are generally faster 而不是 std::shared_ptrs,但是如果你的性能要求真的很严格,注册指针可以是 "disabled"(自动替换为它们的原始指针对应物)和编译时指令,允许它们仅在 debug/test/beta 模式下使用(并产生开销)。

因此,如果要有一个基于原始指针的 "observer" 指针,那么可以说应该有一个基于注册指针的指针,也许正如 OP 建议的那样,一个也基于 std::shared_ptr .

proposal 看来,std::observer_ptr 主要用于记录指针是对对象的 非拥有引用,而不是拥有引用数组字符串迭代器

然而,与 T* 相比,使用 observer_ptr<T> 还有一些其他好处:

  1. 默认构造的observer_ptr将始终被初始化为nullptr;常规指针可能会也可能不会被初始化,具体取决于上下文。
  2. observer_ptr 仅支持对 reference 有意义的操作;这强制正确使用:
    • operator[] 没有为 observer_ptr 实现,因为这是一个 array 操作。
    • observer_ptr 无法进行指针运算,因为这些是 迭代器 操作。
  3. 两个 observer_ptrstrict weak ordering on all implementations, which is not guaranteed for two arbitrary pointers. This is because operator< is implemented in terms of std::less for observer_ptr (as with std::unique_ptr and std::shared_ptr).
  4. observer_ptr<void> 似乎不受支持,这可能会鼓励使用更安全的解决方案(例如 std::any and std::variant

除了文档用例之外,在没有观察者装饰的情况下传递原始指针时可能会发生现实世界的问题。其他代码可能错误地承担了原始指针的生命周期责任,并将指针传递给拥有权 std::unique_ptrstd::shared_ptr,或者只是通过 delete.

简单地处理对象

对于可能在所有权规则未完全建立的情况下升级的遗留代码尤其如此。 observer_ptr 有助于强制执行对象生命周期不能转移的规则。

考虑以下示例:

#include <iostream>
#include <memory>

struct MyObject
{
    int value{ 42 };
};

template<typename T>
void handlerForMyObj(T ptr) noexcept
{
    if (42 != ptr->value) {
        // This object has gone rogue. Dispose of it!
        std::cout << "The value must be 42 but it's actually " << ptr->value << "!\n";
        delete ptr;
        return;
    }
    std::cout << "The value is  " << ptr->value << ".\n";
}

void func1()
{
    MyObject myObj;
    MyObject *myObjPtr = &myObj; 

    myObj.value = 24;

    // What?! Likely run-time crash. BOO!
    handlerForMyObj(myObjPtr);
}

void func2()
{
    MyObject myObj;
    std::observer_ptr<MyObject> myObjObserver{ &myObj };

    myObj.value = 24;

    // Nice! Compiler enforced error because ownership transfer is denied!
    handlerForMyObj(myObjObserver);
}

int main(int argn, char *argv[])
{
    func1();
    func2();
}

在原始指针的情况下,对象的错误删除可能只有在运行时才能发现。但是在 observer_ptr 的情况下, delete 运算符不能应用于观察者。

其他人指出 observer_ptr 除了自我记录非所有权之外的各种好处。但是,如果您只对传达非所有权感兴趣,Bjarne Stroustrup 建议在 C++ standards working group paper P1408R0 中为 std::exprimental::observing_ptr 提供一个简洁的替代方案(顺便说一下,他建议放弃 std::observer_ptr):

template<typename T> using observer_ptr = T*;

我想你需要这个 -- noshared_ptr / noweak_ptr

https://github.com/xhawk18/noshared_ptr