C++陷阱 — C++ auto变量类型推导
问题描述
C++ 使用auto类型声明一个单例对象的引用时,通过该auto变量来访问单例,是否等同于使用单例类::Instance()
来访问单例呢?
试看如下的例子:
#include <stdint.h>
#include <iostream>
#include <string>
#include <map>
using namespace std;
class SingleClass
{
public:
~SingleClass(){}
static SingleClass & Instance(){
static SingleClass s_instance;
return s_instance;
}
void insert(pair<string, int> && p){
m_map.insert(p);
}
void printMap(){
for (auto& n : m_map)
cout << n.first << ":" << n.second <<endl;
}
private:
SingleClass(){};
map<string, int> m_map;
};
int main()
{
auto single1 = SingleClass::Instance();
SingleClass & single2 = SingleClass::Instance();
single1.insert({"A", 1});
single1.insert({"B", 2});
single1.insert({"C", 3});
single1.insert({"D", 4});
SingleClass::Instance().insert({"E", 5});
single2.insert({"F", 6});
cout << "========auto single1======="<<endl;
single1.printMap();
cout << "========SingleClass::Instance()======="<<endl;
SingleClass::Instance().printMap();
cout << "========single2======="<<endl;
single2.printMap();
return 0;
}
========auto single1=======
A:1
B:2
C:3
D:4
========SingleClass::Instance()=======
E:5
F:6
========single2=======
E:5
F:6
预想中single1
和single2
、SingleClass::Instance()
一样都是指向同一个单例对象,实际测试结果却并不是这样,single2
和SingleClass::Instance()
指向的是同一个单例对象,single1
指向的却是另一个单例!
为什么会这样,是获取单例函数的问题吗?没有正确的构造单例?打印这些单例引用对象的地址看一下
class SingleClass
{
static SingleClass & Instance(){
static SingleClass s_instance;
cout << "get instance:" << &s_instance <<endl;
return s_instance;
}
...
}
int main()
{
auto single1 = SingleClass::Instance();
SingleClass & single2 = SingleClass::Instance();
single1.insert({"A", 1});
single1.insert({"B", 2});
single1.insert({"C", 3});
single1.insert({"D", 4});
SingleClass::Instance().insert({"E", 5});
single2.insert({"F", 6});
cout << "========auto single1=======:"<< &single1 <<endl;
single1.printMap();
cout << "==SingleClass::Instance()==:" << &SingleClass::Instance()<<endl;
SingleClass::Instance().printMap();
cout << "==========single2=========:"<< &single2 <<endl;
single2.printMap();
return 0;
}
get instance:0x6052c0
get instance:0x6052c0
get instance:0x6052c0
========auto single1=======:0x7fff0934a430
A:1
B:2
C:3
D:4
get instance:0x6052c0
==SingleClass::Instance()==:0x6052c0
get instance:0x6052c0
E:5
F:6
==========single2=========:0x6052c0
E:5
F:6
很明显单例引用对象single1
和single2
指向的内存地址不一致,也就是他们引用的不是同一个对象!
原因分析
局部静态变量的生命周期是整个程序周期,本代码是在一个线程中顺序执行,在一些弱顺序内存模型的cpu中,cpu(或者编译器)优化后的指令重排也仅影响多线程下的程序,所以本测试代码的单例构造函数不存在线程安全问题。
最有可能的问题是这行的代码,这里把一个引用重新赋值给一个auto变量时,猜测这里可能执行了拷贝构造。是否如此呢,我们测试一下。
auto single1 = SingleClass::Instance();
实现一下拷贝构造,看看打印输出:
SingleClass(SingleClass & r){
cout << "copy construct:" << &r << endl;
};
输出
get instance:0x6042c0
copy construct:0x6042c0
get instance:0x6042c0
get instance:0x6042c0
========auto single1=======:0x7ffff734a8d0
A:1
B:2
C:3
D:4
========SingleClass::Instance()=======:0x6042c0
get instance:0x6042c0
E:5
F:6
========single2=======:0x6042c0
E:5
F:6
在第一个get instance
的后面,紧接着打印了 copy construct
。
所以明显的, 单例构造后,在auto变量single1
赋值时发生了拷贝构造,导致内存中存在两份“单例”。
总结
当单例构造返回的是引用类型时,且使用auto类型推导时,要特别注意禁用拷贝构造函数和赋值构造函数。
禁用方式有两种,一是使用delete
关键字,二是可以声明为private
//单例类。需要禁用拷贝构造,否则在使用auto声明单例的引用时将会产生问题
SingleClass(SingleClass & r) = delete;
//一般单例也需要禁用赋值构造函数
SingleClass & operator = (const SingleClass &) = delete;
当单例构造返回的是指针时,只要保证线程安全,不禁用拷贝构造也不会存在上述的问题。