今天敲代码的时候遇到 STL 的一个 bug,与 C++ 的类中的 const 成员变量有关。什么,明明提供了默认的构造函数和复制构造函数,竟然还要类提供赋值运算符重载。怎么会这样?

测试代码 Test.cpp 如下:

#include <vector>
#include <iostream> class Mass
{
private:
const float x; public:
explicit Mass(float x)
:x(x)
{
std::cout<<"constructor"<<std::endl;
} Mass(const Mass &rhs)
:x(rhs.x)
{
std::cout<<"copy constructor"<<std::endl;
} ~Mass()
{
std::cout<<"destructor"<<std::endl;
} void print() const
{
std::cout<<x<<std::endl;
}
}; int main()
{
Mass m(12.0f);
m.print(); std::vector<Mass> v;
v.push_back(m); return ;
}

代码很简单,一个描述物质质量的类 Mass 和使用 std::vector,用 GCC 编译上面代码 gcc Test.cpp -o Test -lstdc++ 报错如下:

Test.cpp: In instantiation of ‘void std::vector<_Tp, _Alloc>::_M_insert_aux(std::vector<_Tp, _Alloc>::iterator, const _Tp&) [with _Tp = Mass; _Alloc = std::allocator<Mass>; std::vector<_Tp, _Alloc>::iterator = __gnu_cxx::__normal_iterator<Mass*, std::vector<Mass> >; typename std::_Vector_base<_Tp, _Alloc>::pointer = Mass*]’:
/usr/include/c++/4.7/bits/stl_vector.h:893:4: required from ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = Mass; _Alloc = std::allocator<Mass>; std::vector<_Tp, _Alloc>::value_type = Mass]’
Test.cpp:50:15: required from here
Test.cpp:13:7: error: non-static const member ‘const float Mass::x’, can’t use default assignment operator
In file included from /usr/include/c++/4.7/vector:70:0,
from Test.cpp:7:
/usr/include/c++/4.7/bits/vector.tcc:336:4: note: synthesized method ‘Mass& Mass::operator=(const Mass&)’ first required here

error: non-static const member ‘const float Mass::x’, can’t use default assignment operator
const(还有 reference 成员变量)成员变量的初始化必须用初始化列表,只有构造函数才可以用初始化列表,赋值操作符(operator=)是不被允许的。默认的赋值操作符仅完成位拷贝,而 const 成员变量又不允许覆写。编译器也很希望它是 static const member,这样的话变量是属于类而不是对象,就不用复制了。如此一来,似乎含有 const 成员变量的类不允许出现赋值操作符重载的。

于是又用没有 const 成员变量,而有 operator= 的类测试一下。

class Foo
{
private:
int bar; public:
Foo(int bar):bar(bar) {printf("constructor\n");}
Foo(const Foo &rhs):bar(rhs.bar) {printf("copy constructor\n");}
Foo& operator=(const Foo &rhs) {printf("assignment\n"); bar=rhs.bar; return *this;}
~Foo() {printf("destructor\n");}
};

这次能编译通过,输出结果也是意料之中的。

constructor
copy constructor
destructor
destructor

疑问出来了,他只是调用复制构造函数,怎么上面的代码调用 std::vector 的 push_back 函数时使用了赋值操作?还好 STL 都是用模板实现的,可以查看源码一探究竟。push_back 的实现在 /usr/include/c++/4.7/bits/stl_vector.h。(4.7是版本号,你的 GCC 版本很可能是 4.6,但相对路径不变。)

// /usr/include/c++/4.6/bits/stl_vector.h
void push_back(const value_type& __x)
{
if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
{
this->_M_impl.construct(this->_M_impl._M_finish, __x);
++this->_M_impl._M_finish;
}
else
_M_insert_aux(end(), __x);
}

上面的代码很明了,如果容器(vector)的当前容量(capacity)未满,便在当前位置构造一个新的对象,并递增一下体积(size),期间调用 copy constructor 和 placement new 操作符;否则,调用 _M_insert_aux 函数插入。很显然,容器未满时,走第一条路线,但是这里没有使用 operator = 呀!再看看 _M_insert_aux 函数内容,该函数也被 insert(iterator __position, const value_type& __x) 所调用。里面的代码格式比较乱,我稍微整理了一下。

// /usr/include/c++/4.7/bits/vector.tcc
template<typename _Tp, typename _Alloc>
void vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x)
{
if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
{
this->_M_impl.construct(this->_M_impl._M_finish, _GLIBCXX_MOVE(*(this->_M_impl._M_finish - )));
++this->_M_impl._M_finish; #ifndef __GXX_EXPERIMENTAL_CXX0X__
_Tp __x_copy = __x;
#endif
_GLIBCXX_MOVE_BACKWARD3(__position.base(), this->_M_impl._M_finish - , this->_M_impl._M_finish - );
#ifndef __GXX_EXPERIMENTAL_CXX0X__
*__position = __x_copy;
#else
*__position = _Tp(std::forward<_Args>(__args)...);
#endif
}
else
{
const size_type __len = _M_check_len(size_type(), "vector::_M_insert_aux");
const size_type __elems_before = __position - begin();
pointer __new_start(this->_M_allocate(__len));
pointer __new_finish(__new_start);
... // reallocating code part }
}

跟上面的 push_back 函数一样,先检查是否已满,未满的话复制对象到末尾,并递增一下体积。代码中的 _GLIBCXX_MOVE 是 GCC 的扩展,也是 C++0x 实现的 move 语义。不懂的可以抽时间了解一下 C++ 新的特性——右值引用和 move 语义(rvalue reference & move semantics),这里也有解释右值应用的,可以细细品味。

#ifdef __GXX_EXPERIMENTAL_CXX0X__
# define _GLIBCXX_MOVE3(_Tp, _Up, _Vp) std::move(_Tp, _Up, _Vp)
# define _GLIBCXX_MOVE(__val) std::move(__val)
# define _GLIBCXX_FORWARD(_Tp, __val) std::forward<_Tp>(__val)
#else
# define _GLIBCXX_MOVE3(_Tp, _Up, _Vp) std::copy(_Tp, _Up, _Vp)
# define _GLIBCXX_MOVE(__val) (__val)
# define _GLIBCXX_FORWARD(_Tp, __val) (__val)
#endif

没有启用宏 __GXX_EXPERIMENTAL_CXX0X__ 时,_Tp __x_copy = __x; 句调用构造函数,*__position = __x_copy; 句嗲用赋值操作符。
但是,等等,等一等,从代码执行流程来看,push_back 的 if 判断语句为 true 的话,_M_insert_aux 是不会执行的。然而,我却要为 operator= “买单”!而且即使实现了 operator=,也不会被调用,有点不近人情。
      事情也不是没有解决方法的,一个可行的替代方案(workaround)是在上面的 Mass 类添加如下 operator= 实现,直接返回自己,反正类中的 const 成员变量不会改变。(如果有其他非 const 变量,可以拷贝过来。)编译也能通过。看!我们重载 operator = 成功了,欢呼一下。

Mass& operator=(const Mass &rhs)
{
std::cout<<"assignment"<<std::endl;
// non_const_member=rhs.non_const_member;
return *this;
}

再次回想一下,为了避免调用赋值操作,如果仅仅是析构掉原来位置上旧的元素,再构造新的元素填充,似乎显得办事没有效率。
注意到上面的宏 __GXX_EXPERIMENTAL_CXX0X__,如果启用(-std=c++0x)的话就可以避免了,可以利用 C++0x 的 std::move 语义。
gcc version 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)             编译失败;
gcc version 4.7.3 (Ubuntu/Linaro 4.7.3-2ubuntu1~12.04) 编译通过。
重新编译一下 gcc Test.cpp -o Test -lstdc++ -std=c++0x
很好,这次编译过了。。

标准模板库(STL)的一个 bug的更多相关文章

  1. 标准模板库(STL)学习探究之vector容器

    标准模板库(STL)学习探究之vector容器  C++ Vectors vector是C++标准模板库中的部分内容,它是一个多功能的,能够操作多种数据结构和算法的模板类和函数库.vector之所以被 ...

  2. C++ 标准模板库(STL)

    C++ 标准模板库(STL)C++ STL (Standard Template Library标准模板库) 是通用类模板和算法的集合,它提供给程序员一些标准的数据结构的实现如 queues(队列), ...

  3. STL学习系列之一——标准模板库STL介绍

    库是一系列程序组件的集合,他们可以在不同的程序中重复使用.C++语言按照传统的习惯,提供了由各种各样的函数组成的库,用于完成诸如输入/输出.数学计算等功能. 1. STL介绍 标准模板库STL是当今每 ...

  4. 标准模板库--STL

    标准模板库STL 1.泛型程序设计 C++ 语言的核心优势之一就是便于软件的重用 C++中有两个方面体现重用: 1.面向对象的思想:继承和多态,标准类库 2.泛型程序设计(generic progra ...

  5. C++的标准模板库STL中实现的数据结构之顺序表vector的分析与使用

    摘要 本文主要借助对C++的标准模板库STL中实现的数据结构的学习和使用来加深对数据结构的理解.即联系数据结构的理论分析和详细的应用实现(STL),本文是系列总结的第一篇,主要针对线性表中的顺序表(动 ...

  6. C++的标准模板库STL中实现的数据结构之链表std::list的分析与使用

    摘要 本文主要借助对C++的标准模板库STL中实现的数据结构的学习和使用来加深对数据结构的理解,即联系数据结构的理论分析和详细的应用实现(STL),本文是系列总结的第二篇.主要针对线性表中的链表 ST ...

  7. C++ 标准模板库STL 队列 queue 使用方法与应用介绍

    C++ 标准模板库STL 队列 queue 使用方法与应用介绍 queue queue模板类的定义在<queue>头文件中. 与stack模板类很相似,queue模板类也需要两个模板参数, ...

  8. 标准模板库(STL)学习探究之stack

    标准模板库(STL)学习探究之stack queue priority_queue list map/multimap dequeue string

  9. 实验8 标准模板库STL

    一.实验目的与要求: 了解标准模板库STL中的容器.迭代器.函数对象和算法等基本概念. 掌握STL,并能应用STL解决实际问题. 二.实验过程: 完成实验8标准模板库STL中练习题,见:http:// ...

随机推荐

  1. 泛型-List<T>

    声明一个int类型的泛型对象 List<int> list=new List<int>(); list.Add(44);//不用装箱 int i1=list[0];//也不会发 ...

  2. Caused by: java.sql.BatchUpdateException: Transaction error, need to rollback. errno:1205 Lock wait timeout exceeded; try restarting transaction

    更新的时候报 Caused by: java.sql.BatchUpdateException: Transaction error, need to rollback. errno:1205 Loc ...

  3. 关于分页接口设计(下拉刷新上拉加载原理,解决page count请求重复数据的问题)

  4. zabbix监控单核cpu使用率和多核cpu总负载

    zabbix自带的基础监控的模板中只有对单核cpu负载1分钟.5分钟.15分钟的监控. 添加对总的cpu负载的监控 key:system.cpu.load[all,avg1] 1分钟cpu总的负载 添 ...

  5. Tomcat基本入门知识及发布,虚拟访问及启动碰到的错误,虚拟目录,虚拟路径,各种Tomcat的配置

    Tomcat容器入门介绍 转自javaresearch.com由timgball 整理 Tomcat是一个免费的开源Web服务器,最新版本是5.5.1,支持Servlet2.4,JSP2.0,非常适合 ...

  6. SpringMVC配置拦截器实现登录控制

    SpringMVC读取Cookie判断用户是否登录,对每一个action都要进行判断.之前使用jstl标签在页面上判断session如果没有登录就使用如下代码跳转到登录页面. <c:if tes ...

  7. 大熊君{{bb}}移动开发之旅(第一季)

    一,开篇概述 Hi,大家好!大熊君又和大家见面了,从这篇文章开始我要和大家聊聊移动开发的话题,这部分文章共8季,分别从不同角度来讲解什么是移动开发?移动开发涉及到什么方面的技术点以及移动开发中的常见问 ...

  8. jQuery如何退出each循环的?

    试问:jQuery是如何退出each循环的? 在回调函数里return false即可,大多数jQuery的方法都是如此的. 返回 'false'  , 将停止循环 (就像在普通的循环中使用 'bre ...

  9. PHP如何判断一个gif图片是否为动画?

    首先想到的是用getimagesize()看看type,发现都是gif. 然后想gif动画是gif89格式的,发现文件开头是gif89,但是很多透明图片也是用的gif89格式. 看来必须分析文件的祯了 ...

  10. Linux文件查找命令 find 详解

    关于find命令 由于find具有强大的功能,所以它的选项也很多,其中大部分选项都值得我们花时间来了解一下.即使系统中含有网络文件系统( NFS),find命令在该文件系统中同样有效,只你具有相应的权 ...