48

std::observer_ptr库基础技术规范 V2中的构造到底有什么意义?

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

在我的所有代码std::unique_ptr中,我都使用了需要明确拥有对象以及std::shared_ptr可以共享对象所有权的位置。

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

std::observer_ptr当然,不保证观察到的对象的生命周期。

如果它是由 a 构造的,std::unique_ptr或者std::shared_ptr我会在这样的结构中看到一个用途,但是任何简单使用的代码T*都可能会继续这样做,并且如果他们计划移动到任何它会std::shared_ptr和/或std::unique_ptr(取决于使用时)。


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

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

如果它阻止智能指针在被观察时破坏其存储的对象,这将是有用的。

但是如果我想观察 astd::shared_ptr或者std::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){}

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

它只是用于自我记录的来源吗?

4

8 回答 8

39

提案非常清楚地表明它只是用于自我记录:

本文提出observer_ptr了一种(不是非常)智能指针类型,它对其指针对象(即它所观察的对象)不承担所有权责任。因此,它旨在作为原始指针类型的近乎直接的替代品,其优点是,作为一种词汇类型,它表明了它的预期用途,而无需代码阅读器进行详细分析。

于 2015-08-06T17:53:10.720 回答
35

当您需要共享访问但不需要共享所有权时。

问题是原始指针仍然非常有用,并且具有非常受人尊敬的用例场景。

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

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

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

这是一个函数说“给我你的指针,我不会干预它的分配,我只是用它来观察”的一种方式。

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

附加说明:

这是与std::shared_ptr. 这std::shared_ptr是关于共享所有权,只有在您无法确定哪个拥有对象将首先超出范围时才应使用它。

std::observer_ptr另一方面,当您想要共享访问权限而不是所有权时。

std::shared_ptr仅仅使用共享访问是不合适的,因为这可能非常低效。

因此,无论您是使用 a还是 a管理目标指针,原始指针仍然有一个用例,因此 a 是合理的。std::unique_ptrstd::shared_ptrstd::observer_ptr

于 2015-08-06T18:11:56.207 回答
23

它只是用于源自文档吗?

是的。

于 2015-08-06T17:44:21.710 回答
22

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

但是,使用observer_ptr<T>over还有其他一些好处T*

  1. 默认构造的observer_ptr将始终初始化为nullptr; 常规指针可能会或可能不会被初始化,具体取决于上下文。
  2. observer_ptr仅支持对参考有意义的操作;这强制正确使用:
    • operator[]没有实现observer_ptr,因为这是一个数组操作。
    • 指针算术是不可能的observer_ptr,因为这些是迭代器操作。
  3. 两个observer_ptrs 在所有实现上都具有严格的弱排序,这不能保证两个任意指针。这是因为operator<是根据std::lessfor observer_ptr(与std::unique_ptrand一样std::shared_ptr)实现的。
  4. observer_ptr<void>似乎不受支持,这可能会鼓励使用更安全的解决方案(例如std::anystd::variant
于 2016-03-09T17:02:43.537 回答
6

使用std::observer_ptrover raw 指针的一个很好的结果是,它为从 C 继承的令人困惑和容易出错的多指针实例化语法提供了更好的替代方案。

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

是对

int *a, *b, *c;

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

int* a, b, c;
于 2015-08-06T18:49:06.817 回答
2

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

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

不幸的是,对于使用原始指针作为“观察者”指针的安全性,似乎存在一种普遍但在我看来是不切实际的乐观主义。特别是,虽然很容易声明目标对象必须比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>参数的目标在完成之前被释放使用它。

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

如果用or替换std::observer_ptr<employee_t>参数由于某种原因是不可接受的,那么现在有另一个安全选项 - 这是答案的无耻插入部分 - “注册指针”。“注册指针”是智能指针,其行为类似于原始指针,除了它们(自动)设置为目标对象被销毁时,默认情况下,如果您尝试访问已被删除的对象,则会引发异常. 它们通常更快std::shared_ptrstd::weak_ptrnull_ptr比 std::shared_ptrs,但如果您的性能要求非常严格,注册指针可以通过编译时指令“禁用”(自动替换为对应的原始指针),从而允许在调试中使用(并产生开销)仅限 /test/beta 模式。

因此,如果将有一个基于原始指针的“观察者”指针,那么可以说应该有一个基于注册指针的指针,也许正如 OP 所建议的那样,一个基于 std::shared_ptr 的指针。

于 2016-02-19T15:08:29.760 回答
1

除了文档用例之外,在没有观察者修饰的情况下传递原始指针时可能会发生现实世界的问题。其他代码可能错误地为原始指针承担终身责任,并将指针传递给所有权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运算符不能应用于观察者。

于 2019-11-07T03:19:37.883 回答
0

其他人指出了observer_ptr除了自我记录非所有权之外的各种好处。但是,如果您只对传达非所有权感兴趣,Bjarne Stroustrupstd::exprimental::observing_ptrC++ 标准工作组文件 P1408R0中提出了一个简洁的替代方案(顺便说一句,他建议放弃std::observer_ptr):

template<typename T> using observer_ptr = T*;
于 2020-12-09T13:33:43.460 回答