今天敲代码的时候遇到 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. JavaScript自运行函数(function(){})()的理解

    今天打开JQuery源文件(jquery-1.8.3), 看到JQuery的初始化过程是这样的 (function( window, undefined ) { // .... })( window ...

  2. Map工具系列-05-添加业务参数工具

    所有cs端工具集成了一个工具面板 -打开(IE) Map工具系列-01-Map代码生成工具说明 Map工具系列-02-数据迁移工具使用说明 Map工具系列-03-代码生成BySQl工具使用说明 Map ...

  3. hadoop在网页客户端的maven配置

    hadoop网页客户端maven配置,只能在tomcat7上运行,tomcat6和tomcat8运行会出错,我用的是tomcat-7.0.67 完整的pom.xml内容为: <!-- 这个配置只 ...

  4. VisualStudio配色方案

    最近发现一个很神奇的网站,可以方便的为VisualStudio配色:Studio Styles - Visual Studio color schemes 可以下载一份自己喜欢的配色方案 如果还不满意 ...

  5. microsoft docx document operation with Java POI library

    microsoft docx document operation with Java POI library combine multiple docx document into one docu ...

  6. WPF DataGrid绑定到数据源的方法

    string conStr = System.Configuration.ConfigurationManager.ConnectionStrings["str"].Connect ...

  7. Day2-python基础2

    本次学习内容 1.for循环 2.continue and break 3.while循环 4.运算符 5.数据类型 数字 字符串 列表 1.for循环 猜年龄的游戏完善下,只有三次机会 for i ...

  8. Html中行内样式的设置

    Html中行内样式的设置.. <html> <head> <title>显示的页面选项卡标题</title> <style type=" ...

  9. 图解JVM的Class文件格式(详细版)

          了解JAVA的Class文件结构有助于掌握JAVA语言的底层运行机制,我在学习的过程中会不断的与ELF文件格式作对比(当然他们的复杂程度.格式相去甚远,比如可执行ELF的符号表解析在静态链 ...

  10. css3 动画效果 定义和绑定执行

    首先要定义一个动画效果  keyframes 关键字 这里动画效果执行完毕后 恢复本身的css样式  有的动画效果 移动到位置 要保持 就需要写好css 元素的位置 css里直接写  (这里是一般的 ...