C++同一原始指针的多个唯一指针

12

请考虑下面的代码。我对unique指针的理解是只有一个unique指针可以用来引用一个变量或对象。在我的代码中,我有多个unique_ptr访问同一个变量。

显然这不是正确使用智能指针的方式,因为指针应该从创建时就具有完全所有权。但是,为什么这是有效的而没有编译错误?谢谢。

#include <iostream>
#include <memory>

using namespace std;

int main()
{
    int val = 0;
    int* valPtr = &val;

    unique_ptr <int> uniquePtr1(valPtr);
    unique_ptr <int> uniquePtr2(valPtr);

    *uniquePtr1 = 10;
    *uniquePtr2 = 20;

    return 0;
}

为什么这是有效的?在VS中,它是无效的并会触发assert。
- Killzone Kid
@KillzoneKid:使用/MTd标志。 - Christian Hackl
@ChristianHackl 是的,尽管如此仍然崩溃。 - Killzone Kid
@KillzoneKid:是的,当然,我只是想指出这不是编译器的默认行为。如果没有设置标志,即使使用相同的*.exe文件连续执行几次,您可能会遇到崩溃问题,也可能不会。 - Christian Hackl
4
为什么这个问题被踩了?如果你不熟悉C ++,这是一个非常有效的关注点。 - Christian Hackl
我刚刚检查了一下,看看clang-tidy是否会标记它。没有。 - user240515
3个回答

10

仍然需要说明,这个为什么是有效的。

它是无效的! 这是未定义的行为,因为std::unique_ptr的析构函数将释放一个具有自动存储期的对象。

实际上,您的程序尝试三次销毁int对象。首先是通过uniquePtr2,然后是通过uniquePtr1,最后是通过val本身。

而没有编译错误?

因为此类错误通常在编译时无法检测到:

unique_ptr <int> uniquePtr1(valPtr);
unique_ptr <int> uniquePtr2(function_with_runtime_input());
在这个例子中,function_with_runtime_input() 可能执行许多复杂的运行时操作,最终返回一个指针给与 valPtr 指向同一对象的指针。
如果你正确使用 std::unique_ptr,那么你几乎总是会使用 std::make_unique,它可以避免这种错误。

4

在Christian Hackl的优秀回答中补充一点:

std::unique_ptr的引入是为了确保指针的RAII;这意味着,与原始指针相反,您不再需要自己负责销毁。智能指针完成了原始指针的整个管理。由于忘记delete而导致的泄漏不再可能发生。

如果std::unique_ptr只允许通过std::make_unique来创建,那么在分配和释放方面它将绝对安全,并且当然也可以在编译时检测到。

但实际情况并非如此:std::unique_ptr还可以使用原始指针构造。原因是,能够使用硬指针进行构造使得std::unique_ptr更加有用。如果这不可能,例如Christian Hackl的function_with_runtime_input()返回的指针将无法集成到现代RAII环境中,您必须自己负责销毁。

当然,这样做的缺点是像您这样的错误可能会发生:使用std::unique_ptr忘记销毁是不可能的,但如果您使用原始指针构造函数参数创建它,则多次错误销毁总是可能的(并且编译器无法跟踪,正如C.H.已经说过的那样)。请始终注意,std::unique_ptr在逻辑上“拥有”原始指针 - 这意味着除了std::unique_ptr本身之外,没有人可以删除该指针。
作为经验法则,可以说:
  • 如有可能,始终使用std::make_unique创建std::unique_ptr
  • 如果需要使用原始指针构造,请在创建std::unique_ptr后不再操作原始指针。
  • 请注意,std::unique_ptr将接管提供的原始指针的所有权。
  • 仅向堆提供原始指针。永远不要使用指向本地堆栈变量的原始指针(因为它们将不可避免地被自动销毁,例如您示例中的val)。
  • 如果可能,仅使用由new创建的原始指针创建std::unique_ptr
  • 如果需要使用由其他方式创建的原始指针来构造std::unique_ptr,请向std::unique_ptr添加与硬指针创建者匹配的自定义删除器。一个例子是(C语言)FreeImage库中的图像指针,它们总是需要通过FreeImage_Unload()进行销毁。
一些关于这些规则的例子:
// Safe
std::unique_ptr<int> p = std::make_unique<int>();

// Safe, but not advisable. No accessible raw pointer exists, but should use make_unique. 
std::unique_ptr<int> p(new int());

// Handle with care. No accessible raw pointer exists, but it has to be sure
// that function_with_runtime_input() allocates the raw pointer with 'new'
std::unique_ptr<int> p( function_with_runtime_input() );

// Safe. No accessible raw pointer exists,
// the raw pointer is created by a library, and has a custom
// deleter to match the library's requirements
struct FreeImageDeleter {
    void operator() (FIBITMAP* _moribund) { FreeImage_Unload(_moribund); }
};
std::unique_ptr<FIBITMAP,FreeImageDeleter> p( FreeImage_Load(...) );

// Dangerous. Your class method gets a raw pointer
// as a parameter. It can not control what happens
// with this raw pointer after the call to MyClass::setMySomething()
// - if the caller deletes it, your'e lost.
void MyClass::setMySomething( MySomething* something ) {
   // m_mySomethingP is a member std::unique_ptr<Something>
   m_mySomethingP = std::move( std::unique_ptr<Something>( something ));
}

// Dangerous. A raw pointer variable exists, which might be erroneously
// deleted multiple times or assigned to a std::unique_ptr multiple times.
// Don't touch iPtr after these lines!
int* iPtr = new int();
std::unique_ptr<int> p(iPtr);

// Wrong (Undefined behaviour) and a direct consequence of the dangerous declaration above.
// A raw pointer is assigned to a std::unique_ptr<int> twice, which means
// that it will be attempted to delete it twice.
// This couldn't have happened if iPtr wouldn't have existed in the first
// place, like shown in the 'safe' examples.
int* iPtr = new int();
std::unique_ptr<int> p(iPtr);
std::unique_ptr<int> p2(iPtr);


// Wrong. (Undefined behaviour)
// An unique pointer gets assigned a raw pointer to a stack variable.
// Erroneous double destruction is the consequence
int val;
int* valPtr = &val;
std::unique_ptr<int> p(valPtr);

0
这段代码的示例有些人为。在现实世界的代码中,通常不会以这种方式初始化unique_ptr。使用std::make_unique或者初始化unique_ptr时不要存储原始指针到一个变量中:
unique_ptr <int> uniquePtr2(new int);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接