库基础技术规范V2中的构造std::observer_ptr
到底有什么意义?
在我看来,它所做的只是包裹一个裸T*
,如果它不增加动态内存安全性,这似乎是一个多余的步骤。
在我的所有代码中,我使用std::unique_ptr
,我需要明确拥有对象,std::shared_ptr
我可以共享对象的所有权。
这非常有效并且可以防止意外取消引用已经被破坏的对象。
std::observer_ptr
无法保证所观察对象的生命周期。
如果它是从std::unique_ptr
或std::shared_ptr
构建的,我会看到在这样的结构中使用,但任何只使用T*
的代码可能只是保留这样做,如果他们打算搬到任何地方,那就是std::shared_ptr
和/或std::unique_ptr
(取决于使用情况)。
给出一个简单的示例函数:
template<typename T>
auto func(std::observer_ptr<T> ptr){}
如果智能指针在被观察时停止销毁它们的存储对象,那将是有用的。
但如果我想观察std::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){}
那么,这个新结构的用途是什么?
仅用于自我记录来源吗?
答案 0 :(得分:32)
proposal非常清楚它只是用于自我记录:
本文提出
observer_ptr
,一种不是所有权的(非常)智能指针类型 它的责任,即它所观察的物体的责任。因此,它旨在作为附近 原始指针类型的直接替换,其优点是作为词汇表类型 表示其预期用途,无需代码阅读器进行详细分析。
答案 1 :(得分:28)
当您需要共享访问但不需要共享所有权。
问题是原始指针仍然非常有用,并且具有完全可观的用例场景。
当原始指针由智能指针管理时,它的清理得到保证,因此,在智能指针的生命周期内,它通过智能指针正在管理的原始指针访问实际数据是有意义的。
因此,当我们创建通常采用原始指针的函数时,一种承诺函数不会删除该指针的好方法是使用类似std::observer_ptr
的强类型类。
将托管的原始指针作为参数传递给std::observer_ptr
函数参数时,我们知道该函数不会delete
它。
这是一种函数说出&#34;给我指针的方式,我不会干涉它的分配,我只会用它来观察&#34;。
顺便说一下,我并不热衷于名字std::observer_ptr
,因为这意味着你可以看但不能触摸。但事实并非如此。我会更喜欢access_ptr
。
附加说明:
这是与std::shared_ptr
不同的用例。 std::shared_ptr
是关于共享所有权,当您无法确定哪个拥有对象超出范围时,应该仅第一
另一方面,std::observer_ptr
适用于您希望共享访问而非所有权的时间。
仅仅使用std::shared_ptr
来共享访问并不合适,因为这可能效率很低。
因此,无论您是使用std::unique_ptr
还是std::shared_ptr
管理目标指针,原始指针仍然存在用例因而是std::observer_ptr
的理性。
答案 2 :(得分:18)
是
答案 3 :(得分:11)
std::observer_ptr
似乎observer_ptr<T>
主要用于记录指针是对象的非拥有引用,而不是拥有引用,数组,字符串或迭代器。
然而,使用T*
优于observer_ptr
还有其他一些好处:
nullptr
将始终初始化为observer_ptr
;可能会或可能不会初始化常规指针,具体取决于上下文。operator[]
仅支持对引用有意义的操作;这强制正确使用:
observer_ptr
未针对observer_ptr
实施,因为这是数组操作。observer_ptr
无法使用指针运算,因为它们是迭代器操作。operator<
在所有实现上都有proposal,不能保证两个任意指针。这是因为observer_ptr
是针对observer_ptr<void>
的{{3}}实施的(与strict weak ordering和std::less
一样)。Something
似乎不受支持,这可能会鼓励使用更安全的解决方案(例如std::unique_ptr
和std::shared_ptr
)答案 4 :(得分:5)
在原始指针上使用std::observer_ptr
的一个好结果是,它为从C继承的容易混淆且容易出错的多指针实例化语法提供了更好的替代方法。
std::observer_ptr<int> a, b, c;
是对
的改进int *a, *b, *c;
从C ++的角度来看有点奇怪,很容易被误输为
int* a, b, c;
答案 5 :(得分:2)
是的,std::observer_ptr
的重点主要是&#34;自我记录&#34;这本身就是一个有效的目的。但应该指出,可以说它并没有做得很好,因为它并不是一个明确的观察者&#34;指针是。首先,正如Galik所指出的,对某些人而言,这个名称似乎意味着承诺不修改目标,这不是意图,所以像access_ptr
这样的名称会更好。第二,没有任何限定词,这个名字就意味着支持它的非功能性和#34;行为。例如,人们可能会认为std::weak_ptr
是一种&#34;观察者&#34;指针。但是std::weak_ptr
通过提供允许尝试访问(解除分配的)对象以安全地失败的机制来适应指针超过目标对象的情况。 std::observer_ptr
的实施不适合这种情况。所以也许raw_access_ptr
会是一个更好的名称,因为它会更好地表明其功能缺陷。
所以,正如你有理由问的那样,这个功能上受到挑战的是什么?非拥有者&#34;指针?主要原因可能是表现。许多C ++程序员认为std::share_ptr
的开销过高,因此只需在需要时使用原始指针&#34;观察者&#34;指针。建议的std::observer_ptr
尝试以可接受的性能成本提供代码清晰度的小幅改进。具体而言,零性能成本。
不幸的是,似乎普遍存在,但在我看来,对于使用原始指针作为观察者&#34;以及使用原始指针的安全性是不切实际的乐观主义。指针。特别是,虽然很容易说明目标对象必须比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::observer_ptr<employee_t>
或std::shared_ptr
替换std::weak_ptr
参数是出于任何不可接受的原因,那么现在有另一个安全选项 - 这是答案的无耻插件部分 - &#34; registered pointers&#34 ;. &#34;注册指针&#34;智能指针的行为就像原始指针一样,除了它们在目标对象被销毁时(自动)设置为null_ptr
,并且默认情况下,如果您尝试访问已经存在的对象,则会抛出异常删除。它们通常是faster而不是std :: shared_ptrs,但如果您的性能要求非常严格,则注册指针可以被禁用&#34; (使用其原始指针对应方自动替换)使用编译时指令,允许它们仅在调试/测试/测试模式下使用(并产生开销)。
所以,如果有一个&#34;观察者&#34;指针基于原始指针,然后可以说应该有一个基于已注册的指针,也许可以作为OP建议,一个基于std :: shared_ptr。
答案 6 :(得分:0)
除了文档用例之外,在未经观察者修饰的情况下传递原始指针时,还会发生现实问题。其他代码可能会错误地承担原始指针的生命周期责任,并将该指针传递给拥有std::unique_ptr
,std::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
运算符不能应用于观察者。
答案 7 :(得分:0)
其他人指出了observer_ptr
的各种好处,除了自我证明非所有权。但是,如果您只想传达非所有权,Bjarne Stroustrup建议在C++ standards working group paper P1408R0中使用std::exprimental::observing_ptr
的简洁替代方案(他偶然建议放弃std::observer_ptr
): / p>
template<typename T> using observer_ptr = T*;