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

      今天敲代码的时候遇到 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 0; }

代码很简单,一个描述物质质量的类 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 - 1)));
        ++this->_M_impl._M_finish;
        
#ifndef __GXX_EXPERIMENTAL_CXX0X__
        _Tp __x_copy = __x;
#endif
        _GLIBCXX_MOVE_BACKWARD3(__position.base(), this->_M_impl._M_finish - 2, this->_M_impl._M_finish - 1);
#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(1), "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
很好,这次编译过了。。



转载于:https://www.cnblogs.com/Martinium/p/STL_bug.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值