标准模板库(STL)的一个 bug

时间:2021-07-24 19:18:48

今天敲代码的时候遇到 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
很好,这次编译过了。。