单例模式:auto_ptr和unique_ptr的不同行为

Singleton pattern: different behavior of auto_ptr and unique_ptr

本文关键字:ptr auto 单例模式 unique      更新时间:2023-10-16

在实现工厂类时,我遇到了我无法理解的std::auto_ptr的行为。我把这个问题简化成下面这个小程序,所以…我们开始吧。

考虑以下单例类:

singleton.h

#ifndef SINGLETON_H_
#define SINGLETON_H_
#include<iostream>
#include<memory>
class singleton {
public:
  static singleton* get() {
    std::cout << "singleton::get()" << std::endl;
    if ( !ptr_.get() ) {
      std::cout << &ptr_ << std::endl;
      ptr_.reset( new singleton  );
      std::cout << "CREATED" << std::endl;
    }
    return ptr_.get();
  }
  ~singleton(){
    std::cout << "DELETED" << std::endl;
  }
private:
  singleton() {}
  singleton(const singleton&){}
  static std::auto_ptr< singleton > ptr_;
  //static std::unique_ptr< singleton > ptr_;
};
#endif

singleton.cpp

#include<singleton.h>o
std::auto_ptr< singleton > singleton::ptr_(0);
//std::unique_ptr< singleton > singleton::ptr_;

这里使用智能指针来管理资源主要是为了避免程序退出时的泄漏。我在下面的程序中使用了这段代码:

a.h

#ifndef A_H_
#define A_H_
int foo();
#endif

a.cpp

#include<singleton.h>
namespace {
  singleton * dummy( singleton::get() );
}
int foo() {  
  singleton * pt = singleton::get();
  return 0;
}

main.cpp

#include<a.h>
int main() {
  int a = foo();
  return 0;
}
现在是有趣的部分。我分别编译了这三个源代码:
$ g++  -I./ singleton.cpp -c 
$ g++  -I./ a.cpp -c 
$ g++  -I./ main.cpp -c

如果我按以下顺序显式链接它们:

$ g++ main.o singleton.o a.o

一切都按照我的预期工作,并且我得到以下标准输出:

singleton::get()
0x804a0d4
CREATED
singleton::get()
DELETED

如果我使用以下顺序链接源:

$ g++ a.o main.o singleton.o

我得到这样的输出:

singleton::get()
0x804a0dc
CREATED
singleton::get()
0x804a0dc
CREATED
DELETED

我尝试了不同的编译器品牌(Intel和GNU)和版本,这种行为在它们之间是一致的。无论如何,我无法看到其行为取决于链接顺序的代码。

此外,如果auto_ptrunique_ptr取代,行为总是与我期望的正确行为一致。

这让我想到了一个问题:有人知道这里发生了什么吗?

dummystd::auto_ptr< singleton > singleton::ptr_(0)的构造顺序未指定

对于auto_ptr的情况,如果先构造dummy然后再构造singleton::ptr_(0),则dummy调用中创建的值将被ptr_(0)的构造函数擦除。

我会通过ptr_(([](){ std::cout << "made ptr_n"; }(),0));或类似的东西在ptr_的构造中添加跟踪。

它与unique_ptr一起工作的事实是巧合,可能是由于unique_ptr(0)可以计算出它是零的优化,因为这样什么都不做(static数据在构造开始之前被归零,所以如果编译器可以计算出unique_ptr(0)只是将内存归零,它可以合法地跳过构造函数,这意味着您不再将内存归零)。

解决这个问题的一种方法是在使用之前使用保证构造的方法,例如:
   static std::auto_ptr< singleton >& get_ptr() {
     static std::auto_ptr< singleton > ptr_(0);
     return ptr_;
   }
ptr_的引用替换为get_ptr()

在不同翻译单元中定义的文件作用域对象的构造顺序未指定。然而,典型地,在连接在另一个翻译单元之前的翻译单元中定义的对象在第二个翻译单元中定义的对象之前构造。这里的不同之处在于a.osingleton.o链接的顺序。当singleton.oa.o之前被链接时,singleton::ptr_dummy之前被初始化,一切正常。当a.o首先被链接时,dummy首先被初始化,这就构造了单例;然后将singleton::ptr_初始化为0,丢弃指向singleton原始副本的指针。然后在对foo的调用中,对singleton::get()的调用再次构造单例。