• 标准模板库(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
    ");}
        Foo(const Foo &rhs):bar(rhs.bar) {printf("copy constructor
    ");}
        Foo& operator=(const Foo &rhs) {printf("assignment
    "); bar=rhs.bar; return *this;}
        ~Foo() {printf("destructor
    ");}
    };

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

    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
    很好,这次编译过了。。



  • 相关阅读:
    在浏览器中输入URL并回车后都发生了什么?
    HashMap数据结构
    记录一次mysql死锁
    常见排序(归并排序)
    记录一次redis事故
    jsp与javaBean
    org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [com.zhuoshi.entity.Dep#1]
    Oracle创建表空间报错:O/S-Error: (OS 3) 系统找不到指定的路径
    在myeclipse中maven项目关于ssh整合时通过pom.xml导入依赖是pom.xml头部会报错
    2018idea如何布置tomcat修改URL后连接不到
  • 原文地址:https://www.cnblogs.com/Martinium/p/STL_bug.html
Copyright © 2020-2023  润新知